# Hoody API Reference — Full

Generated: 2026-06-15T23:03:12.776Z
Pages: 322

Every endpoint documented in the Hoody API reference, concatenated into
a single file suitable for uploading to an LLM or downloading offline.

---

<!-- === agent-branches.mdx === -->
## Agent:Branches

_Source: `src/content/docs/api/agent-branches.mdx`_

## 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.mdx === -->
## Agent:Config

_Source: `src/content/docs/api/agent-config.mdx`_

## 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.mdx === -->
## Agent:Experimental

_Source: `src/content/docs/api/agent-experimental.mdx`_

## 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.mdx === -->
## Agent:Files

_Source: `src/content/docs/api/agent-files.mdx`_

## 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.mdx === -->
## Agent:Image Gen

_Source: `src/content/docs/api/agent-image-gen.mdx`_

## 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.mdx === -->
## Agent:Mcp

_Source: `src/content/docs/api/agent-mcp.mdx`_

:::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.mdx === -->
## Agent:Memory

_Source: `src/content/docs/api/agent-memory.mdx`_

## 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.mdx === -->
## Agent:Meta

_Source: `src/content/docs/api/agent-meta.mdx`_

## 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.mdx === -->
## Agent:Orchestration

_Source: `src/content/docs/api/agent-orchestration.mdx`_

## 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.mdx === -->
## Agent:Permissions

_Source: `src/content/docs/api/agent-permissions.mdx`_

## 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.mdx === -->
## Agent:Project

_Source: `src/content/docs/api/agent-project.mdx`_

## 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.mdx === -->
## Agent:Prompt

_Source: `src/content/docs/api/agent-prompt.mdx`_

## 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.mdx === -->
## Agent:Providers

_Source: `src/content/docs/api/agent-providers.mdx`_

## 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.mdx === -->
## Agent:Questions

_Source: `src/content/docs/api/agent-questions.mdx`_

## 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.mdx === -->
## Agent:Sessions

_Source: `src/content/docs/api/agent-sessions.mdx`_

## 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.mdx === -->
## Agent:Skills

_Source: `src/content/docs/api/agent-skills.mdx`_

## 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.mdx === -->
## Agent:Web Search

_Source: `src/content/docs/api/agent-web-search.mdx`_

## 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.mdx === -->
## Agent:Workspace

_Source: `src/content/docs/api/agent-workspace.mdx`_

## 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.mdx === -->
## AI Models

_Source: `src/content/docs/api/ai-models.mdx`_

{/* 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.mdx === -->
## Api:Activity Logs

_Source: `src/content/docs/api/api-activity-logs.mdx`_

## 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.mdx === -->
## Api:AI

_Source: `src/content/docs/api/api-ai.mdx`_

## 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.mdx === -->
## Api:Auth Tokens

_Source: `src/content/docs/api/api-auth-tokens.mdx`_

## 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.mdx === -->
## Api:Authentication

_Source: `src/content/docs/api/api-authentication.mdx`_

## 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.mdx === -->
## Api:Billing

_Source: `src/content/docs/api/api-billing.mdx`_

## 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.mdx === -->
## Api:Container Environment

_Source: `src/content/docs/api/api-container-environment.mdx`_

## 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.mdx === -->
## Api:Container Firewall

_Source: `src/content/docs/api/api-container-firewall.mdx`_

## 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.mdx === -->
## Api:Container Images

_Source: `src/content/docs/api/api-container-images.mdx`_

## 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.mdx === -->
## Api:Containers

_Source: `src/content/docs/api/api-containers.mdx`_

## 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.mdx === -->
## Api:Events

_Source: `src/content/docs/api/api-events.mdx`_

## 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.mdx === -->
## Api:Meta

_Source: `src/content/docs/api/api-meta.mdx`_

## 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.mdx === -->
## Api:Notifications

_Source: `src/content/docs/api/api-notifications.mdx`_

## 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.mdx === -->
## Api:Pool Invitations

_Source: `src/content/docs/api/api-pool-invitations.mdx`_

## 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.mdx === -->
## Api:Pool Members

_Source: `src/content/docs/api/api-pool-members.mdx`_

## 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.mdx === -->
## Api:Pools

_Source: `src/content/docs/api/api-pools.mdx`_

## 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.mdx === -->
## Api:Projects

_Source: `src/content/docs/api/api-projects.mdx`_

## 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.mdx === -->
## Api:Realms

_Source: `src/content/docs/api/api-realms.mdx`_

## 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.mdx === -->
## Api:Rentals

_Source: `src/content/docs/api/api-rentals.mdx`_

## 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.mdx === -->
## Api:Server Commands

_Source: `src/content/docs/api/api-server-commands.mdx`_

## 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.mdx === -->
## Api:Server Rental

_Source: `src/content/docs/api/api-server-rental.mdx`_

## 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.mdx === -->
## Api:Storage Shares

_Source: `src/content/docs/api/api-storage-shares.mdx`_

## 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.mdx === -->
## Api:Two-Factor Authentication

_Source: `src/content/docs/api/api-two-factor-authentication.mdx`_

## 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.mdx === -->
## Api:User Vault

_Source: `src/content/docs/api/api-user-vault.mdx`_

## 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.mdx === -->
## Api:Users

_Source: `src/content/docs/api/api-users.mdx`_

## 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.mdx === -->
## Api:Utilities

_Source: `src/content/docs/api/api-utilities.mdx`_

## 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.mdx === -->
## Api:Wallet

_Source: `src/content/docs/api/api-wallet.mdx`_

## 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-api-documentation.mdx === -->
## App:API Documentation

_Source: `src/content/docs/api/app-api-documentation.mdx`_

## 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.mdx === -->
## App:App Execution

_Source: `src/content/docs/api/app-app-execution.mdx`_

## 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.mdx === -->
## App:Configuration

_Source: `src/content/docs/api/app-configuration.mdx`_

## 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.mdx === -->
## App:Health

_Source: `src/content/docs/api/app-health.mdx`_

## 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.mdx === -->
## App:Jobs

_Source: `src/content/docs/api/app-jobs.mdx`_

## 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.mdx === -->
## App:Profiles

_Source: `src/content/docs/api/app-profiles.mdx`_

## 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.mdx === -->
## App:Recipes

_Source: `src/content/docs/api/app-recipes.mdx`_

## 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.mdx === -->
## App:Sources

_Source: `src/content/docs/api/app-sources.mdx`_

## 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
```

---

<!-- === auth-tokens.mdx === -->
## API Tokens

_Source: `src/content/docs/api/auth-tokens.mdx`_

{/* 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.mdx === -->
## Authentication

_Source: `src/content/docs/api/authentication.mdx`_

{/* 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 |

---

<!-- === browser-browser-interaction.mdx === -->
## Browser:Browser Interaction

_Source: `src/content/docs/api/browser-browser-interaction.mdx`_

## 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.mdx === -->
## Browser:Browser State

_Source: `src/content/docs/api/browser-browser-state.mdx`_

## 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.mdx === -->
## Browser:Browsing History

_Source: `src/content/docs/api/browser-browsing-history.mdx`_

## 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.mdx === -->
## Browser:Debugging

_Source: `src/content/docs/api/browser-debugging.mdx`_

## 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.mdx === -->
## Browser:Instance Management

_Source: `src/content/docs/api/browser-instance-management.mdx`_

## 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.mdx === -->
## Browser:Introspection & Control

_Source: `src/content/docs/api/browser-introspection-control.mdx`_

## 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.mdx === -->
## Browser:Page Content

_Source: `src/content/docs/api/browser-page-content.mdx`_

## 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.mdx === -->
## Browser:Server Health & Metrics

_Source: `src/content/docs/api/browser-server-health-metrics.mdx`_

## 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
```

---

<!-- === code-auth.mdx === -->
## Code:auth

_Source: `src/content/docs/api/code-auth.mdx`_

## 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.mdx === -->
## Code:extensions

_Source: `src/content/docs/api/code-extensions.mdx`_

## 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.mdx === -->
## Code:health

_Source: `src/content/docs/api/code-health.mdx`_

## 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.mdx === -->
## Code:proxy

_Source: `src/content/docs/api/code-proxy.mdx`_

## 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.mdx === -->
## Code:static

_Source: `src/content/docs/api/code-static.mdx`_

## 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.mdx === -->
## Code:vscode

_Source: `src/content/docs/api/code-vscode.mdx`_

## 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.mdx === -->
## Container Copy & Sync

_Source: `src/content/docs/api/container-copy-sync.mdx`_

{/* 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-env.mdx === -->
## Container Environment Variables

_Source: `src/content/docs/api/container-env.mdx`_

{/* 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.mdx === -->
## Container Firewall

_Source: `src/content/docs/api/container-firewall.mdx`_

{/* 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.mdx === -->
## Container Images

_Source: `src/content/docs/api/container-images.mdx`_

{/* 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.mdx === -->
## Container Network

_Source: `src/content/docs/api/container-network.mdx`_

{/* 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.mdx === -->
## Container Operations & Lifecycle

_Source: `src/content/docs/api/container-operations.mdx`_

{/* 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.mdx === -->
## Container Snapshots

_Source: `src/content/docs/api/container-snapshots.mdx`_

{/* 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.mdx === -->
## Containers

_Source: `src/content/docs/api/containers.mdx`_

{/* 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 |

---

<!-- === cron-crontab.mdx === -->
## Cron:crontab

_Source: `src/content/docs/api/cron-crontab.mdx`_

:::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.mdx === -->
## Cron:entries

_Source: `src/content/docs/api/cron-entries.mdx`_

:::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.mdx === -->
## Cron:Health

_Source: `src/content/docs/api/cron-health.mdx`_

:::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.mdx === -->
## Cron:system

_Source: `src/content/docs/api/cron-system.mdx`_

:::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
```

---

<!-- === curl-curl.mdx === -->
## Curl:curl

_Source: `src/content/docs/api/curl-curl.mdx`_

## 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.mdx === -->
## Curl:events

_Source: `src/content/docs/api/curl-events.mdx`_

## 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.mdx === -->
## Curl:Health

_Source: `src/content/docs/api/curl-health.mdx`_

## 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.mdx === -->
## Curl:jobs

_Source: `src/content/docs/api/curl-jobs.mdx`_

## 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.mdx === -->
## Curl:ops

_Source: `src/content/docs/api/curl-ops.mdx`_

## 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.mdx === -->
## Curl:schedules

_Source: `src/content/docs/api/curl-schedules.mdx`_

## 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.mdx === -->
## Curl:sessions

_Source: `src/content/docs/api/curl-sessions.mdx`_

## 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.mdx === -->
## Curl:storage

_Source: `src/content/docs/api/curl-storage.mdx`_

## 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
```

---

<!-- === daemon-control.mdx === -->
## Daemon:Control

_Source: `src/content/docs/api/daemon-control.mdx`_

## 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.mdx === -->
## Daemon:Health

_Source: `src/content/docs/api/daemon-health.mdx`_

## 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.mdx === -->
## Daemon:Programs

_Source: `src/content/docs/api/daemon-programs.mdx`_

## 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.mdx === -->
## Daemon:Quick Start

_Source: `src/content/docs/api/daemon-quick-start.mdx`_

## 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.mdx === -->
## Daemon:Status

_Source: `src/content/docs/api/daemon-status.mdx`_

## 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.mdx === -->
## Display:Display

_Source: `src/content/docs/api/display-display.mdx`_

## 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.mdx === -->
## Display:Health

_Source: `src/content/docs/api/display-health.mdx`_

## 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.mdx === -->
## Display:Input

_Source: `src/content/docs/api/display-input.mdx`_

## 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.mdx === -->
## Display:Screenshots

_Source: `src/content/docs/api/display-screenshots.mdx`_

## 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.mdx === -->
## Display:Thumbnails

_Source: `src/content/docs/api/display-thumbnails.mdx`_

## 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
```

---

<!-- === exec-cache.mdx === -->
## Exec:Cache

_Source: `src/content/docs/api/exec-cache.mdx`_

## 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.mdx === -->
## Exec:Dependencies

_Source: `src/content/docs/api/exec-dependencies.mdx`_

## 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.mdx === -->
## Exec:Health

_Source: `src/content/docs/api/exec-health.mdx`_

## 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.mdx === -->
## Exec:List

_Source: `src/content/docs/api/exec-list.mdx`_

## 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.mdx === -->
## Exec:Logs

_Source: `src/content/docs/api/exec-logs.mdx`_

## 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.mdx === -->
## Exec:Magic-comments

_Source: `src/content/docs/api/exec-magic-comments.mdx`_

## 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.mdx === -->
## Exec:Monitor

_Source: `src/content/docs/api/exec-monitor.mdx`_

## 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.mdx === -->
## Exec:Package

_Source: `src/content/docs/api/exec-package.mdx`_

## 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.mdx === -->
## Exec:Route

_Source: `src/content/docs/api/exec-route.mdx`_

## 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.mdx === -->
## Exec:Schedules

_Source: `src/content/docs/api/exec-schedules.mdx`_

## 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.mdx === -->
## Exec:Script Execution

_Source: `src/content/docs/api/exec-script-execution.mdx`_

## 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.mdx === -->
## Exec:Scripts

_Source: `src/content/docs/api/exec-scripts.mdx`_

## 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.mdx === -->
## Exec:Sdk

_Source: `src/content/docs/api/exec-sdk.mdx`_

## 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.mdx === -->
## Exec:Shared-state

_Source: `src/content/docs/api/exec-shared-state.mdx`_

## 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.mdx === -->
## Exec:System

_Source: `src/content/docs/api/exec-system.mdx`_

## 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.mdx === -->
## Exec:Templates

_Source: `src/content/docs/api/exec-templates.mdx`_

## 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.mdx === -->
## Exec:User-openapi

_Source: `src/content/docs/api/exec-user-openapi.mdx`_

## 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.mdx === -->
## Exec:Validate

_Source: `src/content/docs/api/exec-validate.mdx`_

## 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
```

---

<!-- === files-archives.mdx === -->
## Files:Archives

_Source: `src/content/docs/api/files-archives.mdx`_

## 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.mdx === -->
## Files:Authentication

_Source: `src/content/docs/api/files-authentication.mdx`_

## 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.mdx === -->
## Files:Backends

_Source: `src/content/docs/api/files-backends.mdx`_

## 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.mdx === -->
## Files:Directories

_Source: `src/content/docs/api/files-directories.mdx`_

## 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.mdx === -->
## Files:Downloads

_Source: `src/content/docs/api/files-downloads.mdx`_

## 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.mdx === -->
## Files:Files

_Source: `src/content/docs/api/files-files.mdx`_

## 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.mdx === -->
## Files:Health

_Source: `src/content/docs/api/files-health.mdx`_

## 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.mdx === -->
## Files:Image Processing

_Source: `src/content/docs/api/files-image-processing.mdx`_

## 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.mdx === -->
## Files:Journal

_Source: `src/content/docs/api/files-journal.mdx`_

## 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.mdx === -->
## Files:Mounts

_Source: `src/content/docs/api/files-mounts.mdx`_

## 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.mdx === -->
## Files:Remote - FTP

_Source: `src/content/docs/api/files-remote-ftp.mdx`_

## 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.mdx === -->
## Files:Remote - Git

_Source: `src/content/docs/api/files-remote-git.mdx`_

## 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.mdx === -->
## Files:Remote - S3

_Source: `src/content/docs/api/files-remote-s3.mdx`_

## 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.mdx === -->
## Files:Remote - SSH

_Source: `src/content/docs/api/files-remote-ssh.mdx`_

## 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.mdx === -->
## Files:System

_Source: `src/content/docs/api/files-system.mdx`_

## 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-web-dav.mdx === -->
## Files:WebDAV

_Source: `src/content/docs/api/files-web-dav.mdx`_

## 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.mdx === -->
## Files:Webdav

_Source: `src/content/docs/api/files-webdav.mdx`_

## 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.mdx === -->
## General

_Source: `src/content/docs/api/general.mdx`_

## 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
```

---

<!-- === hoody-daemon.mdx === -->
## Daemon Manager

_Source: `src/content/docs/api/hoody-daemon.mdx`_

{/* 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.

---

<!-- === notes-avatars.mdx === -->
## Notes:avatars

_Source: `src/content/docs/api/notes-avatars.mdx`_

## 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.mdx === -->
## Notes:collaborators

_Source: `src/content/docs/api/notes-collaborators.mdx`_

## 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.mdx === -->
## Notes:comments

_Source: `src/content/docs/api/notes-comments.mdx`_

## 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.mdx === -->
## Notes:databases

_Source: `src/content/docs/api/notes-databases.mdx`_

## 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.mdx === -->
## Notes:documents

_Source: `src/content/docs/api/notes-documents.mdx`_

## 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.mdx === -->
## Notes:files

_Source: `src/content/docs/api/notes-files.mdx`_

## 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.mdx === -->
## Notes:health

_Source: `src/content/docs/api/notes-health.mdx`_

## 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.mdx === -->
## Notes:identity

_Source: `src/content/docs/api/notes-identity.mdx`_

## 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.mdx === -->
## Notes:interactions

_Source: `src/content/docs/api/notes-interactions.mdx`_

## 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.mdx === -->
## Notes:mutations

_Source: `src/content/docs/api/notes-mutations.mdx`_

## 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.mdx === -->
## Notes:nodes

_Source: `src/content/docs/api/notes-nodes.mdx`_

## 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.mdx === -->
## Notes:notebooks

_Source: `src/content/docs/api/notes-notebooks.mdx`_

## 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.mdx === -->
## Notes:reactions

_Source: `src/content/docs/api/notes-reactions.mdx`_

## 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.mdx === -->
## Notes:sockets

_Source: `src/content/docs/api/notes-sockets.mdx`_

## 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.mdx === -->
## Notes:users

_Source: `src/content/docs/api/notes-users.mdx`_

## 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.mdx === -->
## Notes:versions

_Source: `src/content/docs/api/notes-versions.mdx`_

## 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.mdx === -->
## Notifications:Health

_Source: `src/content/docs/api/notifications-health.mdx`_

## 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.mdx === -->
## Notifications:Icons

_Source: `src/content/docs/api/notifications-icons.mdx`_

## 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.mdx === -->
## Notifications:Notifications

_Source: `src/content/docs/api/notifications-notifications.mdx`_

## 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.mdx === -->
## Notifications:Notify

_Source: `src/content/docs/api/notifications-notify.mdx`_

## 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.mdx === -->
## Notifications & Events

_Source: `src/content/docs/api/notifications.mdx`_

{/* 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.mdx === -->
## Pipe:Health

_Source: `src/content/docs/api/pipe-health.mdx`_

## 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.mdx === -->
## Pipe:info

_Source: `src/content/docs/api/pipe-info.mdx`_

## 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.mdx === -->
## Pipe:pipe

_Source: `src/content/docs/api/pipe-pipe.mdx`_

## 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.mdx === -->
## Pipe:ui

_Source: `src/content/docs/api/pipe-ui.mdx`_

## 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
```

---

<!-- === pipe.mdx === -->
## Hoody Pipe

_Source: `src/content/docs/api/pipe.mdx`_

{/* 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.mdx === -->
## Projects

_Source: `src/content/docs/api/projects.mdx`_

{/* 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.mdx === -->
## Proxy Aliases

_Source: `src/content/docs/api/proxy-aliases.mdx`_

{/* 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.mdx === -->
## Proxy Discovery

_Source: `src/content/docs/api/proxy-discovery.mdx`_

{/* 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.mdx === -->
## Proxy Hooks

_Source: `src/content/docs/api/proxy-hooks.mdx`_

{/* 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",
});
```

---

<!-- === proxy-logs-logs-config.mdx === -->
## ProxyLogs:logs-config

_Source: `src/content/docs/api/proxy-logs-logs-config.mdx`_

## 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
```

---

<!-- === proxy-logs-logs.mdx === -->
## ProxyLogs:logs

_Source: `src/content/docs/api/proxy-logs-logs.mdx`_

## 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
```

---

<!-- === proxy-logs-maintenance.mdx === -->
## ProxyLogs:maintenance

_Source: `src/content/docs/api/proxy-logs-maintenance.mdx`_

## 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
```

---

<!-- === proxy-logs-routing.mdx === -->
## ProxyLogs:routing

_Source: `src/content/docs/api/proxy-logs-routing.mdx`_

## 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.mdx === -->
## Proxy Logs

_Source: `src/content/docs/api/proxy-logs.mdx`_

{/* 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.mdx === -->
## Proxy Permissions

_Source: `src/content/docs/api/proxy-permissions.mdx`_

{/* 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.mdx === -->
## Proxy Settings

_Source: `src/content/docs/api/proxy-settings.mdx`_

{/* 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.mdx === -->
## Realms (API Isolation)

_Source: `src/content/docs/api/realms.mdx`_

{/* 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.mdx === -->
## Server Management API

_Source: `src/content/docs/api/server-management.mdx`_

{/* 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.mdx === -->
## Servers

_Source: `src/content/docs/api/servers.mdx`_

{/* 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" }
});
```

---

<!-- === sqlite-database.mdx === -->
## Sqlite:Database

_Source: `src/content/docs/api/sqlite-database.mdx`_

## 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.mdx === -->
## Sqlite:Documentation

_Source: `src/content/docs/api/sqlite-documentation.mdx`_

## 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.mdx === -->
## Sqlite:Health

_Source: `src/content/docs/api/sqlite-health.mdx`_

## 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.mdx === -->
## Sqlite:History

_Source: `src/content/docs/api/sqlite-history.mdx`_

## 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.mdx === -->
## Sqlite:KV Store

_Source: `src/content/docs/api/sqlite-kv-store.mdx`_

## 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.mdx === -->
## Sqlite:Query

_Source: `src/content/docs/api/sqlite-query.mdx`_

## 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.mdx === -->
## Storage Shares

_Source: `src/content/docs/api/storage-shares.mdx`_

{/* 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-api-documentation.mdx === -->
## Terminal:API Documentation

_Source: `src/content/docs/api/terminal-api-documentation.mdx`_

## 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.mdx === -->
## Terminal:Health

_Source: `src/content/docs/api/terminal-health.mdx`_

## 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.mdx === -->
## Terminal:System Monitoring

_Source: `src/content/docs/api/terminal-system-monitoring.mdx`_

## 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.mdx === -->
## Terminal:Terminal Automation

_Source: `src/content/docs/api/terminal-terminal-automation.mdx`_

## 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.mdx === -->
## Terminal:Terminal Execution

_Source: `src/content/docs/api/terminal-terminal-execution.mdx`_

## 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.mdx === -->
## Terminal:Terminal Sessions

_Source: `src/content/docs/api/terminal-terminal-sessions.mdx`_

## 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.mdx === -->
## Terminal:Terminal

_Source: `src/content/docs/api/terminal-terminal.mdx`_

## 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.mdx === -->
## Tunnel:Health

_Source: `src/content/docs/api/tunnel-health.mdx`_

## 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.mdx === -->
## Tunnel:tunnel

_Source: `src/content/docs/api/tunnel-tunnel.mdx`_

## 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
```

---

<!-- === users.mdx === -->
## User Profile Management

_Source: `src/content/docs/api/users.mdx`_

{/* 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.mdx === -->
## Wallet & Payments

_Source: `src/content/docs/api/wallet.mdx`_

{/* 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.mdx === -->
## Watch:Health

_Source: `src/content/docs/api/watch-health.mdx`_

:::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.mdx === -->
## Watch:streams

_Source: `src/content/docs/api/watch-streams.mdx`_

:::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.mdx === -->
## Watch:watchers

_Source: `src/content/docs/api/watch-watchers.mdx`_

:::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
```

---

<!-- === watch.mdx === -->
## File System Watchers

_Source: `src/content/docs/api/watch.mdx`_

{/* 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 |

---

<!-- === agent/branches.mdx === -->
## Agent: Branches

_Source: `src/content/docs/api/agent/branches.mdx`_

{/* 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.mdx === -->
## Agent: Config

_Source: `src/content/docs/api/agent/config.mdx`_

{/* 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.mdx === -->
## Agent: Experimental

_Source: `src/content/docs/api/agent/experimental.mdx`_

{/* 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.mdx === -->
## Agent: Files

_Source: `src/content/docs/api/agent/files.mdx`_

{/* 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-gen.mdx === -->
## Agent: Image Generation

_Source: `src/content/docs/api/agent/image-gen.mdx`_

{/* 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);
```

---

<!-- === agent/index.mdx === -->
## Hoody Agent

_Source: `src/content/docs/api/agent/index.mdx`_

{/* 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.mdx === -->
## Agent: MCP

_Source: `src/content/docs/api/agent/mcp.mdx`_

{/* 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.mdx === -->
## Agent: Memory

_Source: `src/content/docs/api/agent/memory.mdx`_

{/* 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.mdx === -->
## Agent: Meta

_Source: `src/content/docs/api/agent/meta.mdx`_

{/* 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.mdx === -->
## Agent: MITM Rules

_Source: `src/content/docs/api/agent/mitm.mdx`_

{/* 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.",
  },
});
```

---

<!-- === agent/orchestration.mdx === -->
## Agent: Orchestration

_Source: `src/content/docs/api/agent/orchestration.mdx`_

{/* 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.mdx === -->
## Agent: Permissions

_Source: `src/content/docs/api/agent/permissions.mdx`_

{/* 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.mdx === -->
## Agent: Project

_Source: `src/content/docs/api/agent/project.mdx`_

{/* 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.mdx === -->
## Agent: Prompt

_Source: `src/content/docs/api/agent/prompt.mdx`_

{/* 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.mdx === -->
## Agent: Providers

_Source: `src/content/docs/api/agent/providers.mdx`_

{/* 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.mdx === -->
## Agent: Questions

_Source: `src/content/docs/api/agent/questions.mdx`_

{/* 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-tuning.mdx === -->
## Agent: RSI & Self-Tuning

_Source: `src/content/docs/api/agent/rsi-tuning.mdx`_

{/* 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.mdx === -->
## Agent: Sessions

_Source: `src/content/docs/api/agent/sessions.mdx`_

{/* 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.mdx === -->
## Agent: Skills

_Source: `src/content/docs/api/agent/skills.mdx`_

{/* 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.mdx === -->
## Agent: Web Search

_Source: `src/content/docs/api/agent/web-search.mdx`_

{/* 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.mdx === -->
## Agent: Workspace Sessions

_Source: `src/content/docs/api/agent/workspace-sessions.mdx`_

{/* 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.mdx === -->
## Agent: Workspace

_Source: `src/content/docs/api/agent/workspace.mdx`_

{/* 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",
});
```

---

<!-- === app/execution.mdx === -->
## App: Execution

_Source: `src/content/docs/api/app/execution.mdx`_

{/* 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: {}
```

---

<!-- === app/index.mdx === -->
## Hoody App

_Source: `src/content/docs/api/app/index.mdx`_

{/* 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.mdx === -->
## App: Jobs

_Source: `src/content/docs/api/app/jobs.mdx`_

{/* 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.mdx === -->
## App: Profiles

_Source: `src/content/docs/api/app/profiles.mdx`_

{/* 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.mdx === -->
## App: Recipes

_Source: `src/content/docs/api/app/recipes.mdx`_

{/* 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.mdx === -->
## App: Sources

_Source: `src/content/docs/api/app/sources.mdx`_

{/* 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 |

---

<!-- === browser/control.mdx === -->
## Instance Control

_Source: `src/content/docs/api/browser/control.mdx`_

{/* 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
}
```

---

<!-- === browser/health.mdx === -->
## Health, Metrics & Debugging

_Source: `src/content/docs/api/browser/health.mdx`_

{/* 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.

---

<!-- === browser/index.mdx === -->
## Hoody Browser

_Source: `src/content/docs/api/browser/index.mdx`_

{/* 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.

---

<!-- === browser/instance-management.mdx === -->
## Instance Management

_Source: `src/content/docs/api/browser/instance-management.mdx`_

{/* 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.mdx === -->
## Browser Interaction

_Source: `src/content/docs/api/browser/interaction.mdx`_

{/* 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.

---

<!-- === code/health-monitoring.mdx === -->
## Health Monitoring API

_Source: `src/content/docs/api/code/health-monitoring.mdx`_

{/* 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. |

---

<!-- === code/index.mdx === -->
## Hoody Code Orchestrator

_Source: `src/content/docs/api/code/index.mdx`_

{/* 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
```

---

<!-- === code/instance-management.mdx === -->
## Instance Management API

_Source: `src/content/docs/api/code/instance-management.mdx`_

{/* 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 |

---

<!-- === cron/crontab.mdx === -->
## Raw Crontab

_Source: `src/content/docs/api/cron/crontab.mdx`_

{/* 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.

---

<!-- === cron/entries.mdx === -->
## Managed Entries

_Source: `src/content/docs/api/cron/entries.mdx`_

{/* 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"
```

---

<!-- === cron/index.mdx === -->
## Hoody Cron

_Source: `src/content/docs/api/cron/index.mdx`_

{/* 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 |

---

<!-- === curl/execution.mdx === -->
## HTTP Request Execution

_Source: `src/content/docs/api/curl/execution.mdx`_

{/* 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"
}
```

---

<!-- === curl/index.mdx === -->
## Hoody cURL

_Source: `src/content/docs/api/curl/index.mdx`_

{/* 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
```

---

<!-- === curl/jobs.mdx === -->
## Job Management

_Source: `src/content/docs/api/curl/jobs.mdx`_

{/* 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 |

---

<!-- === curl/scheduling.mdx === -->
## Request Scheduling

_Source: `src/content/docs/api/curl/scheduling.mdx`_

{/* 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");
```

---

<!-- === curl/sessions.mdx === -->
## Session Management

_Source: `src/content/docs/api/curl/sessions.mdx`_

{/* 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.

---

<!-- === curl/storage.mdx === -->
## Storage Management

_Source: `src/content/docs/api/curl/storage.mdx`_

{/* 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.

---

<!-- === daemon/control.mdx === -->
## Program Control

_Source: `src/content/docs/api/daemon/control.mdx`_

{/* 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.

---

<!-- === daemon/index.mdx === -->
## Hoody Daemons

_Source: `src/content/docs/api/daemon/index.mdx`_

{/* 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.mdx === -->
## Daemon Management

_Source: `src/content/docs/api/daemon/management.mdx`_

{/* 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"
}
```

---

<!-- === daemon/monitoring.mdx === -->
## Status & Monitoring

_Source: `src/content/docs/api/daemon/monitoring.mdx`_

{/* 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.

---

<!-- === displays/debugging.mdx === -->
## Displays: Debugging

_Source: `src/content/docs/api/displays/debugging.mdx`_

{/* 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.mdx === -->
## Displays: Feature Flags

_Source: `src/content/docs/api/displays/feature-flags.mdx`_

{/* 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.mdx === -->
## Displays: Health & Info

_Source: `src/content/docs/api/displays/health.mdx`_

{/* 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

---

<!-- === displays/index.mdx === -->
## Hoody Displays

_Source: `src/content/docs/api/displays/index.mdx`_

{/* 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.mdx === -->
## Displays: Input Actions

_Source: `src/content/docs/api/displays/input-actions.mdx`_

{/* 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.mdx === -->
## Displays: Input & Clipboard

_Source: `src/content/docs/api/displays/input-clipboard.mdx`_

{/* 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.mdx === -->
## Displays: Performance & Encoding

_Source: `src/content/docs/api/displays/performance.mdx`_

{/* 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/screenshots.mdx === -->
## Displays: Screenshot API

_Source: `src/content/docs/api/displays/screenshots.mdx`_

{/* 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.mdx === -->
## Displays: Session & Sharing

_Source: `src/content/docs/api/displays/session-sharing.mdx`_

{/* 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.mdx === -->
## Displays: UI & Theming

_Source: `src/content/docs/api/displays/ui-theming.mdx`_

{/* 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.mdx === -->
## Displays: Web Client Interface

_Source: `src/content/docs/api/displays/web-client.mdx`_

{/* 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,
});
```

---

<!-- === exec/ai-generation.mdx === -->
## AI Code Generation

_Source: `src/content/docs/api/exec/ai-generation.mdx`_

{/* 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.

---

<!-- === exec/cache-state.mdx === -->
## Cache & Shared State

_Source: `src/content/docs/api/exec/cache-state.mdx`_

{/* 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.

---

<!-- === exec/dependencies.mdx === -->
## Dependency Management

_Source: `src/content/docs/api/exec/dependencies.mdx`_

{/* 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.

---

<!-- === exec/index.mdx === -->
## Hoody Exec

_Source: `src/content/docs/api/exec/index.mdx`_

{/* 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.

---

<!-- === exec/logs.mdx === -->
## Log Management

_Source: `src/content/docs/api/exec/logs.mdx`_

{/* 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.

---

<!-- === exec/monitoring.mdx === -->
## Monitoring & Performance

_Source: `src/content/docs/api/exec/monitoring.mdx`_

{/* 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.

---

<!-- === exec/package-management.mdx === -->
## Package Management

_Source: `src/content/docs/api/exec/package-management.mdx`_

{/* 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`.

---

<!-- === exec/routing.mdx === -->
## Route Management

_Source: `src/content/docs/api/exec/routing.mdx`_

{/* 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": {}
}
```

---

<!-- === exec/scheduling.mdx === -->
## Schedule Management

_Source: `src/content/docs/api/exec/scheduling.mdx`_

{/* 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.

---

<!-- === exec/script-execution.mdx === -->
## Script Execution

_Source: `src/content/docs/api/exec/script-execution.mdx`_

{/* 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",
});
```

---

<!-- === exec/script-management.mdx === -->
## Script Management

_Source: `src/content/docs/api/exec/script-management.mdx`_

{/* 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}/`.

---

<!-- === exec/templates.mdx === -->
## Script Templates

_Source: `src/content/docs/api/exec/templates.mdx`_

{/* 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",
});
```

---

<!-- === exec/validation.mdx === -->
## Code Validation

_Source: `src/content/docs/api/exec/validation.mdx`_

{/* 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"] }
  }
});
```

---

<!-- === files/backend-connections.mdx === -->
## Backend Connections

_Source: `src/content/docs/api/files/backend-connections.mdx`_

{/* 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.

---

<!-- === files/directories.mdx === -->
## Directory Listing

_Source: `src/content/docs/api/files/directories.mdx`_

{/* 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();
```

---

<!-- === files/downloading.mdx === -->
## Downloading Files

_Source: `src/content/docs/api/files/downloading.mdx`_

{/* 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"
    }
  ]
}
```

---

<!-- === files/file-operations.mdx === -->
## File Operations

_Source: `src/content/docs/api/files/file-operations.mdx`_

{/* 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 |

---

<!-- === files/hashing.mdx === -->
## File Hashing

_Source: `src/content/docs/api/files/hashing.mdx`_

{/* 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.

---

<!-- === files/index.mdx === -->
## Hoody Files

_Source: `src/content/docs/api/files/index.mdx`_

{/* 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();
```

---

<!-- === files/journal.mdx === -->
## File Journal & Audit Log

_Source: `src/content/docs/api/files/journal.mdx`_

{/* 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.

---

<!-- === files/managing-backends.mdx === -->
## Backend Management

_Source: `src/content/docs/api/files/managing-backends.mdx`_

{/* 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 |

---

<!-- === files/metadata.mdx === -->
## File Metadata

_Source: `src/content/docs/api/files/metadata.mdx`_

{/* 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",
});
```

---

<!-- === files/monitoring.mdx === -->
## System Monitoring

_Source: `src/content/docs/api/files/monitoring.mdx`_

{/* 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
```

---

<!-- === files/quick-start.mdx === -->
## Quick Start

_Source: `src/content/docs/api/files/quick-start.mdx`_

{/* 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.

---

<!-- === files/reading.mdx === -->
## Reading Files

_Source: `src/content/docs/api/files/reading.mdx`_

{/* 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.

---

<!-- === files/webdav.mdx === -->
## WebDAV Operations

_Source: `src/content/docs/api/files/webdav.mdx`_

{/* 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"
}
```

---

<!-- === kit/notification-server.mdx === -->
## Notification Server

_Source: `src/content/docs/api/kit/notification-server.mdx`_

{/* 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.

---

<!-- === kit/tunnel.mdx === -->
## Hoody Tunnel

_Source: `src/content/docs/api/kit/tunnel.mdx`_

{/* 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/collaborators.mdx === -->
## Notes: Collaboration

_Source: `src/content/docs/api/notes/collaborators.mdx`_

{/* 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.mdx === -->
## Notes: Comments

_Source: `src/content/docs/api/notes/comments.mdx`_

{/* 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.mdx === -->
## Notes: Databases

_Source: `src/content/docs/api/notes/databases.mdx`_

{/* 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/files.mdx === -->
## Notes: File Uploads

_Source: `src/content/docs/api/notes/files.mdx`_

{/* 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",
});
```

---

<!-- === notes/index.mdx === -->
## Hoody Notes

_Source: `src/content/docs/api/notes/index.mdx`_

{/* 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.mdx === -->
## Notes: Nodes & Documents

_Source: `src/content/docs/api/notes/nodes.mdx`_

{/* 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.mdx === -->
## Notes: Notebooks

_Source: `src/content/docs/api/notes/notebooks.mdx`_

{/* 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/realtime.mdx === -->
## Notes: Real-time & Sync

_Source: `src/content/docs/api/notes/realtime.mdx`_

{/* 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/versions.mdx === -->
## Notes: Document Versions

_Source: `src/content/docs/api/notes/versions.mdx`_

{/* 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>"
```

---

<!-- === sqlite/index.mdx === -->
## Hoody SQLite

_Source: `src/content/docs/api/sqlite/index.mdx`_

{/* 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"
}
```

---

<!-- === sqlite/kv-atomic.mdx === -->
## KV Store: Atomic Operations

_Source: `src/content/docs/api/sqlite/kv-atomic.mdx`_

{/* 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.

---

<!-- === sqlite/kv-basic.mdx === -->
## KV Store: Basic Operations

_Source: `src/content/docs/api/sqlite/kv-basic.mdx`_

{/* 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
});
```

---

<!-- === sqlite/kv-batch.mdx === -->
## KV Store: Batch Operations

_Source: `src/content/docs/api/sqlite/kv-batch.mdx`_

{/* 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/kv-store.mdx === -->
## SQLite: Key-Value Store

_Source: `src/content/docs/api/sqlite/kv-store.mdx`_

{/* 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"
}
```

---

<!-- === sqlite/kv-time-travel.mdx === -->
## KV Store: Time-Travel & History

_Source: `src/content/docs/api/sqlite/kv-time-travel.mdx`_

{/* 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.mdx === -->
## SQLite: SQL Operations

_Source: `src/content/docs/api/sqlite/sql-operations.mdx`_

{/* 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 |

---

<!-- === terminal/automation.mdx === -->
## Terminal: Automation

_Source: `src/content/docs/api/terminal/automation.mdx`_

{/* 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/commands.mdx === -->
## Terminal: Command Execution

_Source: `src/content/docs/api/terminal/commands.mdx`_

{/* 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
  }
});
```

---

<!-- === terminal/index.mdx === -->
## Hoody Terminal

_Source: `src/content/docs/api/terminal/index.mdx`_

{/* 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/monitoring.mdx === -->
## Terminal: System Monitoring

_Source: `src/content/docs/api/terminal/monitoring.mdx`_

{/* 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/sessions.mdx === -->
## Terminal: Session Management

_Source: `src/content/docs/api/terminal/sessions.mdx`_

{/* 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-interface.mdx === -->
## Terminal: Web UI & API Access

_Source: `src/content/docs/api/terminal/web-interface.mdx`_

{/* 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();
```

---

<!-- === agent/orchestration/executor-budget.mdx === -->
## Orchestration: Executor & Budget

_Source: `src/content/docs/api/agent/orchestration/executor-budget.mdx`_

{/* 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.

---

<!-- === agent/orchestration/phases-sessions.mdx === -->
## Orchestration: Phases & Orchestrator Sessions

_Source: `src/content/docs/api/agent/orchestration/phases-sessions.mdx`_

{/* 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.

---

<!-- === agent/orchestration/todo.mdx === -->
## Orchestration: Master TODO

_Source: `src/content/docs/api/agent/orchestration/todo.mdx`_

{/* 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",
});
```

---

<!-- === agent/orchestration/vault-import.mdx === -->
## Orchestration: Vault & Import

_Source: `src/content/docs/api/agent/orchestration/vault-import.mdx`_

{/* 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
}
```

---

<!-- === files/mount/advanced.mdx === -->
## Advanced Backends

_Source: `src/content/docs/api/files/mount/advanced.mdx`_

{/* 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"
}
```

---

<!-- === files/mount/cloud.mdx === -->
## Cloud Storage

_Source: `src/content/docs/api/files/mount/cloud.mdx`_

{/* 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"
  }'
```

---

<!-- === files/mount/encryption.mdx === -->
## Encryption Layer

_Source: `src/content/docs/api/files/mount/encryption.mdx`_

{/* 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.

---

<!-- === files/mount/object.mdx === -->
## Object Storage

_Source: `src/content/docs/api/files/mount/object.mdx`_

{/* 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"
}
```

---

<!-- === files/mount/protocols.mdx === -->
## File Protocols

_Source: `src/content/docs/api/files/mount/protocols.mdx`_

{/* 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"
  }
});
```

---

<!-- === kit/notification-server/fetching.mdx === -->
## Fetching Notifications

_Source: `src/content/docs/api/kit/notification-server/fetching.mdx`_

{/* 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 |

---

<!-- === kit/notification-server/health.mdx === -->
## Health Check

_Source: `src/content/docs/api/kit/notification-server/health.mdx`_

{/* 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.

---

<!-- === kit/notification-server/icons.mdx === -->
## Icons

_Source: `src/content/docs/api/kit/notification-server/icons.mdx`_

{/* 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.

---

<!-- === kit/notification-server/streaming.mdx === -->
## Real-Time Streaming

_Source: `src/content/docs/api/kit/notification-server/streaming.mdx`_

{/* 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.

---

<!-- === kit/notification-server/triggering.mdx === -->
## Triggering Notifications

_Source: `src/content/docs/api/kit/notification-server/triggering.mdx`_

{/* 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"
}
```

---