<!--
hoody-notes Subskill (sdk)
Auto-generated by Hoody Skills Generator
Generated: 2026-06-20T00:09:52.510Z
Model: mimo-v2.5-pro + fixer:z-ai/glm-5.1
Mode: sdk


Tokens: 18062

DO NOT EDIT MANUALLY - Changes will be overwritten on next generation
-->

```
# hoody-notes Subskill

> Complete reference for the Hoody Notes service — a collaborative, structured-notebook platform with rich-text documents, databases, real-time sockets, comments, reactions, and version control.

---

## Overview

### What This Service Does

Hoody Notes is a full-featured collaborative notebook service. It provides hierarchical organization through **notebooks**, **nodes** (pages, sections, channels, messages, databases, records), rich-text **documents**, real-time **WebSocket** communication, **file** management via TUS resumable uploads, **comments** with threading and anchoring, **reactions**, **collaborator** management, **version** control, and structured **database** records.

Each user is auto-provisioned with an identity and a default notebook on first call. Notebooks are the top-level container; everything else lives inside them.

### When To Use It

- You need collaborative, structured note-taking or document editing.
- You want hierarchical content (sections, pages, channels, databases).
- You need real-time updates via WebSocket.
- You require version history, comments, reactions, or collaborator roles.
- You want to manage files with resumable uploads (TUS protocol).
- You need structured records in a database within a notebook.

### How It Fits Into Hoody Philosophy

Hoody Notes follows the Hoody Kit service pattern: it runs as an isolated container service with automatic domain routing, built-in authentication via Hoody API tokens, and zero DNS configuration. Each project and container runs its own isolated notes instance; access is scoped by the Hoody proxy using the caller's API token.

### Service Access Pattern

Hoody Notes is a **Hoody Kit service**, not the Hoody API itself. You interact with it through the Hoody SDK, which automatically resolves the correct service URL from your authenticated session. Never call Hoody API endpoints (`api.hoody.com`) for notes operations — use the `client.notes.*` namespace exclusively.

```
Base URL: https://{projectId}-{containerId}-notes-{serviceId}.{node}.containers.hoody.com
All paths begin with: /api/v1/notes/…
```

---

## Common Workflows

### 1. Health Check and Identity

Always start by verifying service health and retrieving the current user identity. The identity call auto-provisions the user and default notebook on first invocation.

```
import { HoodyClient } from '@hoody-ai/hoody-sdk'

const client = new HoodyClient({
  baseURL: 'https://api.hoody.com',
  token: process.env.HOODY_TOKEN!
})

// Step 1: Verify service is running
const health = await client.notes.health.check()
console.log('Service status:', health.status)
console.log('Build timestamp:', health.buildTimestamp)

// Step 2: Get current user identity (auto-provisions on first call)
const identity = await client.notes.identity.get()
console.log('User ID:', identity.userId)
console.log('Username:', identity.username)
console.log('Role:', identity.role)
console.log('Default notebook:', identity.notebookId)
```

The identity response includes `userId`, `username`, `role`, and `notebookId`. Save the `notebookId` — many subsequent operations require it.

### 2. Notebook Lifecycle

Notebooks are the top-level organizational unit. Create, list, read, update, and delete them.

```
// Step 1: List all notebooks the current user belongs to
const notebooks = await client.notes.notebooks.listNotebooks()
console.log('Notebook count:', notebooks.data.length)

// Step 2: Create a new notebook
const newNotebook = await client.notes.notebooks.create({
  name: 'Project Alpha',
  description: 'Main project notebook',
  avatar: null
})
const notebookId = newNotebook.id
console.log('Created notebook:', notebookId)

// Step 3: Fetch notebook details
const details = await client.notes.notebooks.get(notebookId)
console.log('Name:', details.name)
console.log('Status:', details.status)
console.log('Current user role:', details.role)

// Step 4: Update notebook metadata (owner only)
await client.notes.notebooks.update(notebookId, {
  name: 'Project Alpha v2',
  description: 'Updated project notebook'
})

// Step 5: Verify update
const updated = await client.notes.notebooks.get(notebookId)
console.log('Updated name:', updated.name)

// Step 6: Delete notebook (owner only, permanent)
await client.notes.notebooks.delete(notebookId)
```

**Important**: Only notebook owners can update or delete. Listing excludes notebooks where the user has role `"none"` or inactive status.

### 3. Node Management

Nodes are the building blocks inside a notebook. Types include `section`, `page`, `channel`, `message`, `database`, and `record`.

```
const notebookId = 'your-notebook-id'

// Step 1: Create a section node
const section = await client.notes.nodes.create(notebookId, {
  type: 'section',
  attributes: {
    name: 'Engineering Docs',
    description: 'Technical documentation'
  }
})
console.log('Section ID:', section.id)

// Step 2: Create a page node under the section
const page = await client.notes.nodes.create(notebookId, {
  type: 'page',
  attributes: {
    name: 'Architecture Overview',
    parentId: section.id
  }
})
console.log('Page ID:', page.id)

// Step 3: List nodes with filters
const nodes = await client.notes.nodes.list(
  'page',    // type filter
  section.id, // parentId filter
  undefined,  // rootId filter
  50,         // limit
  0,          // offset
  notebookId
)
console.log('Pages in section:', nodes.data.length)

// Step 4: List direct children of a node
const children = await client.notes.nodes.listChildren(
  20,       // limit
  0,        // offset
  notebookId,
  section.id
)
console.log('Children count:', children.data.length)

// Step 5: Update node attributes
await client.notes.nodes.update(notebookId, page.id, {
  attributes: {
    name: 'System Architecture',
    description: 'Updated architecture doc'
  }
})

// Step 6: Resolve a page by its alias
const byAlias = await client.notes.nodes.getByAlias(notebookId, 'system-architecture')
console.log('Resolved page:', byAlias.id)

// Step 7: Delete a node
await client.notes.nodes.delete(notebookId, page.id)
```

**Constraints**: `type` and `parentId` cannot be changed after creation. Update only modifies `attributes`.

### 4. Document Operations

Documents hold rich-text content attached to nodes. Use these operations to read, write, merge, and append content.

```
const notebookId = 'your-notebook-id'
const nodeId = 'your-node-id'

// Step 1: Get document content
const doc = await client.notes.documents.get(
  undefined,   // blockIds (comma-separated, optional)
  undefined,   // lines range (e.g. "1-100", optional)
  undefined,   // output format (optional, use "html" with ticket)
  undefined,   // includeComments (optional)
  undefined,   // ticket (required for output=html)
  notebookId,
  nodeId
)
console.log('Document blocks:', doc.content.content)

// Step 2: Create or fully replace a document
await client.notes.documents.put(notebookId, nodeId, {
  content: {
    type: 'doc',
    content: [
      {
        type: 'paragraph',
        content: [{ type: 'text', text: 'Hello world' }]
      }
    ]
  }
})

// Step 3: Merge content into existing document (new blocks appended; existing blocks with matching IDs updated in place)
await client.notes.documents.patch(notebookId, nodeId, {
  content: {
    type: 'doc',
    content: [
      {
        type: 'heading',
        attrs: { level: 1 },
        content: [{ type: 'text', text: 'Updated Title' }]
      }
    ]
  }
})

// Step 4: Append blocks to the end of a document
await client.notes.documents.appendDocument(notebookId, nodeId, undefined, {
  text: 'New paragraph',
  blocks: [
    {
      content: [
        {
          type: 'text',
          text: 'Appended content',
          marks: [{ type: 'bold' }]
        }
      ]
    }
  ]
})

// Step 5: Render a drawing block as SVG
const svg = await client.notes.documents.exportBlockSvg(
  '#ffffff',  // background color
  2,          // scale factor
  notebookId,
  nodeId,
  'block-id'
)
console.log('SVG data:', svg)

// Step 6: Create an export ticket for HTML delivery
const ticket = await client.notes.documents.createExportTicket(notebookId, nodeId, {})
console.log('Export ticket:', ticket.ticket)

// Step 7: Fetch HTML using the ticket
const html = await client.notes.documents.get(
  undefined, undefined, 'html', undefined, ticket.ticket, notebookId, nodeId
)
console.log('HTML output:', html)
```

> The export ticket is short-lived — generate it immediately before fetching HTML. Without the ticket, the `output=html` parameter is rejected.

**Key rules for append**: The server assigns each block's `id`, `parentId`, and `index`. Any client-supplied values for these fields are rejected.

### 5. File Management (TUS Protocol)

Files use the TUS resumable upload protocol for reliable large file transfers.

```
const notebookId = 'your-notebook-id'
const fileId = 'generated-file-id' // typically a UUID

// Step 1: Initialize TUS upload
await client.notes.files.tusCreateUpload(notebookId, fileId)

// Step 2: Check upload status
await client.notes.files.tusCheckUpload(notebookId, fileId)

// Step 3: Upload a chunk (repeat for each chunk)
await client.notes.files.tusUploadChunk(notebookId, fileId, chunkBuffer, offset)  // SDK sets Upload-Offset header and Content-Type: application/offset+octet-stream

// Step 4: Abort if needed
await client.notes.files.tusAbortUpload(notebookId, fileId)

// Step 5: List uploaded files
const files = await client.notes.files.list(
  50,        // limit
  0,         // offset
  notebookId
)
console.log('Files count:', files.data.length)

// Step 6: Download a file
const binary = await client.notes.files.download(fileId, notebookId)
console.log('File downloaded, size:', binary.length)
```

### 6. Collaborator Management

Control who can access specific nodes and with what role.

```
const notebookId = 'your-notebook-id'
const nodeId = 'your-node-id'

// Step 1: List current collaborators
const collabs = await client.notes.collaborators.list(notebookId, nodeId)
console.log('Collaborators:', collabs.data.length)

// Step 2: Add a collaborator (requires admin permission)
await client.notes.collaborators.add(notebookId, nodeId, {
  userId: 'target-user-id',
  role: 'editor'
})

// Step 3: Change a collaborator's role
await client.notes.collaborators.update(notebookId, nodeId, 'collaborator-id', {
  role: 'admin'
})

// Step 4: Remove a collaborator
await client.notes.collaborators.remove(notebookId, nodeId, 'collaborator-id')
```

### 7. Comments

Add threaded comments anchored to document content.

```
const notebookId = 'your-notebook-id'
const nodeId = 'your-node-id'

// Step 1: List all comments
const comments = await client.notes.comments.list(
  50,  // limit
  0,   // offset
  undefined, // cursor
  notebookId,
  nodeId
)
console.log('Comments:', comments.data.length)

// Step 2: List comment anchors (lightweight thread metadata)
const anchors = await client.notes.comments.listAnchors(
  50, 0, undefined, notebookId, nodeId
)
console.log('Anchors:', anchors.data.length)

// Step 3: Create a comment
const comment = await client.notes.comments.create(notebookId, nodeId, {
  content: 'This section needs review',
  anchor: { blockId: 'target-block-id', from: 0, to: 10 }
})
console.log('Comment ID:', comment.id)

// Step 4: Edit a comment
await client.notes.comments.edit(notebookId, nodeId, comment.id, {
  content: 'Updated: this section needs urgent review'
})

// Step 5: Resolve a comment thread
await client.notes.comments.resolve(notebookId, nodeId, comment.id, {
  resolved: true
})

// Step 6: Re-anchor a comment thread
await client.notes.comments.reanchor(notebookId, nodeId, comment.id, {
  anchor: { blockId: 'new-block-id', from: 5, to: 15 }
})

// Step 7: Delete a comment
await client.notes.comments.delete(
  undefined, // expectedVersion (optional)
  notebookId,
  nodeId,
  comment.id
)
```

### 8. Reactions and Interactions

Add emoji reactions and track user engagement.

```
const notebookId = 'your-notebook-id'
const nodeId = 'your-node-id'

// Step 1: Add a reaction
await client.notes.reactions.add(notebookId, nodeId, {
  emoji: '👍'
})

// Step 2: List all reactions
const reactions = await client.notes.reactions.list(notebookId, nodeId)
console.log('Reactions:', reactions.data.length)

// Step 3: Remove a reaction
await client.notes.reactions.remove(notebookId, nodeId, '👍')

// Step 4: Mark node as seen
await client.notes.interactions.markSeen(notebookId, nodeId, {})

// Step 5: Mark node as opened
await client.notes.interactions.markOpened(notebookId, nodeId, {})
```

### 9. Version Control

Capture, browse, and restore document versions.

```
const notebookId = 'your-notebook-id'
const nodeId = 'your-node-id'

// Step 1: Capture current document as a version snapshot
const version = await client.notes.versions.create(notebookId, nodeId)
console.log('Version ID:', version.id)

// Step 2: List all versions (ordered by revision descending)
const versions = await client.notes.versions.list(
  20,   // limit
  0,    // offset
  notebookId,
  nodeId
)
console.log('Versions:', versions.data.length)

// Step 3: Get a specific version
const v = await client.notes.versions.get(notebookId, nodeId, 'version-id')
console.log('Version content:', v.content)

// Step 4: Restore a previous version
await client.notes.versions.restore(notebookId, nodeId, 'version-id')
console.log('Document restored to version:', 'version-id')

// Step 5: Delete a version
await client.notes.versions.delete(notebookId, nodeId, 'version-id')
```

### 10. Database Records

Databases are structured storage within notebooks, supporting filtering, sorting, and searching.

```
const notebookId = 'your-notebook-id'
const databaseId = 'your-database-node-id'

// Step 1: Create a record
const record = await client.notes.databases.create(notebookId, databaseId, {
  name: 'Task: Fix login bug',
  fields: {
    status: 'in-progress',
    priority: 'high',
    assignee: 'user-123'
  }
})
console.log('Record ID:', record.id)

// Step 2: List records with filters and sorts
const records = await client.notes.databases.list(
  JSON.stringify({ status: 'in-progress' }),  // filters
  JSON.stringify([{ field: 'priority', dir: 'desc' }]), // sorts
  1,    // page
  25,   // count
  notebookId,
  databaseId
)
console.log('Records:', records.data.length)

// Step 3: Search records by name
const searchResults = await client.notes.databases.search(
  'login',      // query
  undefined,    // exclude (comma-separated IDs)
  notebookId,
  databaseId
)
console.log('Search results:', searchResults.data.length)

// Step 4: Get a single record
const single = await client.notes.databases.get(notebookId, databaseId, record.id)
console.log('Record name:', single.name)

// Step 5: Update a record (fields are merged with existing)
await client.notes.databases.update(notebookId, databaseId, record.id, {
  name: 'Task: Fix login bug — RESOLVED',
  fields: {
    status: 'done',
    resolvedAt: new Date().toISOString()
  }
})

// Step 6: Delete a record
await client.notes.databases.delete(notebookId, databaseId, record.id)
```

### 11. User Management

Invite users to notebooks and manage their roles.

```
const notebookId = 'your-notebook-id'

// Step 1: Invite users by username
const invited = await client.notes.users.invite(notebookId, {
  usernames: ['alice', 'bob']
})
console.log('Created users:', invited.created)
console.log('Errors:', invited.errors)

// Step 2: Change a user's role (requires owner or admin)
await client.notes.users.updateRole(notebookId, 'user-id', {
  role: 'admin'
})
```

### 12. Avatar Management

Upload and retrieve user or notebook avatars.

```
// Step 1: Upload a user-scoped avatar (accepts JPEG, PNG, WebP — resized to 500x500 JPEG; scoped to the authenticated user)
const avatar = await client.notes.avatars.upload()
console.log('Avatar ID:', avatar.id)

// Step 2: Download avatar as JPEG binary
const jpeg = await client.notes.avatars.download(avatar.id)
console.log('Avatar size:', jpeg.length)
```

---

## Advanced Operations

### 13. Real-Time WebSocket Communication

Establish a WebSocket connection for live updates.

```
// Step 1: Initialize a socket session (server-side)
const socket = await client.notes.sockets.init()
console.log('Socket ID:', socket.id)

// Step 2: Open the WebSocket connection
await client.notes.sockets.open(socket.id)
// Connection is now live — listen for real-time events
```

The socket flow requires two steps: first initialize via HTTP to get a `socketId`, then open the WebSocket connection using that ID. This separation allows the server to prepare the session before the upgrade. The `open` call returns a WebSocket instance — use it to attach event listeners, send messages, and close the connection. Refer to the SDK types for supported event names and message schemas.

### 14. Batch Mutations

Process multiple operations atomically in a single request.

```
const notebookId = 'your-notebook-id'

// Batch multiple mutations in one call
const results = await client.notes.mutations.sync(notebookId, {
  mutations: [
    {
      type: 'createNode',
      payload: {
        type: 'page',
        attributes: { name: 'Batched Page 1' }
      }
    },
    {
      type: 'createNode',
      payload: {
        type: 'page',
        attributes: { name: 'Batched Page 2' }
      }
    },
    {
      type: 'addReaction',
      nodeId: 'existing-node-id',
      payload: { emoji: '🎉' }
    },
    {
      type: 'recordInteraction',
      nodeId: 'existing-node-id',
      payload: { kind: 'seen' }
    }
  ]
})

// Check per-mutation status
for (const result of results.data) {
  console.log('Mutation status:', result.status, result.id)
}
```

**Supported mutation types**: node CRUD, reactions, interactions, and document operations. The server processes them in order and returns per-mutation status.

### 15. Paginated Collection Patterns

Many list endpoints support pagination. Use these patterns to fetch all results.

```
const notebookId = 'your-notebook-id'

// Pattern A: Manual pagination with limit/offset
let offset = 0
const limit = 50
let allNodes: any[] = []

while (true) {
  const page = await client.notes.nodes.list(
    undefined, undefined, undefined, limit, offset, notebookId
  )
  allNodes = allNodes.concat(page.data)
  if (page.data.length < limit) break
  offset += limit
}
console.log('Total nodes:', allNodes.length)

// Pattern B: Use SDK convenience method (collects all pages)
const allFiles = await client.notes.files.listAll(50, 0, notebookId)
console.log('All files:', allFiles.length)

// Pattern C: Async iterator for streaming
const iterator = client.notes.files.listIterator(50, 0, notebookId)
for await (const file of iterator) {
  console.log('File:', file.id, file.name)
}
```

The same `listAll` and `listIterator` patterns are available for database records:

```
const allRecords = await client.notes.databases.listAll(
  undefined, undefined, undefined, 25, notebookId, databaseId
)
console.log('All records:', allRecords.length)

const recordIterator = client.notes.databases.listIterator(
  undefined, undefined, undefined, 25, notebookId, databaseId
)
for await (const record of recordIterator) {
  console.log('Record:', record.id, record.name)
}
```

### 16. Drawing Block SVG Export

Export a drawing block as a scalable SVG image.

```
const notebookId = 'your-notebook-id'
const nodeId = 'your-node-id'
const blockId = 'drawing-block-id'

// Export with custom background and scale
const svg = await client.notes.documents.exportBlockSvg(
  '#f5f5f5',  // background color (hex)
  2,           // scale factor (1-4)
  notebookId,
  nodeId,
  blockId
)
// svg contains the SVG markup
```

### 17. Error Recovery Patterns

Handle common failure scenarios gracefully.

```
async function safeNotebookOperation(notebookId: string) {
  try {
    const notebook = await client.notes.notebooks.get(notebookId)
    return notebook
  } catch (error: any) {
    if (error.status === 404) {
      console.error('Notebook not found — may have been deleted')
      // Re-provision by calling identity
      const identity = await client.notes.identity.get()
      return { id: identity.notebookId, recovered: true }
    }
    if (error.status === 403) {
      console.error('Insufficient permissions for this notebook')
      return null
    }
    throw error
  }
}
```

```
// Retry pattern for transient failures
async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await fn()
    } catch (error: any) {
      if (attempt === maxRetries || !error.status || error.status < 500) throw error
      await new Promise(resolve => setTimeout(resolve, 1000 * attempt))
    }
  }
  throw new Error('Unreachable')
}

// Usage
const health = await withRetry(() => client.notes.health.check())
```

### 18. Complete Setup Workflow

A full end-to-end flow from zero to a working notebook with content.

```
import { HoodyClient } from '@hoody-ai/hoody-sdk'

const client = new HoodyClient({
  baseURL: 'https://api.hoody.com',
  token: process.env.HOODY_TOKEN!
})

// 1. Verify service
await client.notes.health.check()

// 2. Get identity (auto-provisions user + default notebook)
const me = await client.notes.identity.get()
const notebookId = me.notebookId

// 3. Create a section
const section = await client.notes.nodes.create(notebookId, {
  type: 'section',
  attributes: { name: 'Getting Started' }
})

// 4. Create a page under the section
const page = await client.notes.nodes.create(notebookId, {
  type: 'page',
  attributes: { name: 'Welcome', parentId: section.id }
})

// 5. Write content to the page
await client.notes.documents.put(notebookId, page.id, {
  content: {
    type: 'doc',
    content: [
      {
        type: 'heading',
        attrs: { level: 1 },
        content: [{ type: 'text', text: 'Welcome to Notes' }]
      },
      {
        type: 'paragraph',
        content: [{ type: 'text', text: 'This is your first page.' }]
      }
    ]
  }
})

// 6. Add a reaction
await client.notes.reactions.add(notebookId, page.id, { emoji: '🚀' })

// 7. Mark as seen
await client.notes.interactions.markSeen(notebookId, page.id, {})

// 8. Create a version snapshot
await client.notes.versions.create(notebookId, page.id)

// 9. Verify the page content
const doc = await client.notes.documents.get(
  undefined, undefined, undefined, undefined, undefined,
  notebookId, page.id
)
console.log('Document has', doc.content.content.length, 'top-level blocks')
```

---

## Quick Reference

### Most Common Endpoints

| Operation | SDK Method | HTTP |
|-----------|-----------|------|
| Health check | `client.notes.health.check()` | GET /api/v1/notes/health |
| Get identity | `client.notes.identity.get()` | GET /api/v1/notes/me |
| List notebooks | `client.notes.notebooks.listNotebooks()` | GET /api/v1/notes/notebooks |
| Create notebook | `client.notes.notebooks.create(data)` | POST /api/v1/notes/notebooks |
| List nodes | `client.notes.nodes.list(type?, parentId?, rootId?, limit?, offset?, notebookId)` | GET /api/v1/notes/notebooks/{notebookId}/nodes |
| Create node | `client.notes.nodes.create(notebookId, data)` | POST /api/v1/notes/notebooks/{notebookId}/nodes |
| Get document | `client.notes.documents.get(blockIds?, lines?, output?, includeComments?, ticket?, notebookId, nodeId)` | GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/document |
| Put document | `client.notes.documents.put(notebookId, nodeId, data)` | PUT /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/document |
| Append blocks | `client.notes.documents.appendDocument(notebookId, nodeId, idempotencyKey?, data)` | POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/document/append |
| List files | `client.notes.files.list(limit?, offset?, notebookId)` | GET /api/v1/notes/notebooks/{notebookId}/files |
| List comments | `client.notes.comments.list(limit?, offset?, cursor?, notebookId, nodeId)` | GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments |
| List versions | `client.notes.versions.list(limit?, offset?, notebookId, nodeId)` | GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/versions |
| List DB records | `client.notes.databases.list(filters?, sorts?, page?, count?, notebookId, databaseId)` | GET /api/v1/notes/notebooks/{notebookId}/databases/{databaseId}/records |
| Init socket | `client.notes.sockets.init()` | POST /api/v1/notes/sockets |
| Sync mutations | `client.notes.mutations.sync(notebookId, data)` | POST /api/v1/notes/notebooks/{notebookId}/mutations |

### Essential Parameters

| Parameter | Used By | Description |
|-----------|---------|-------------|
| `notebookId` | Nearly all endpoints | Target notebook identifier |
| `nodeId` | Nodes, documents, comments, etc. | Target node identifier |
| `type` | `nodes.list()` | Filter by node type: `section`, `page`, `channel`, `message`, `database`, `record` |
| `parentId` | `nodes.list()` | Filter by parent node |
| `limit` / `offset` | Most list endpoints | Pagination controls |
| `filters` | `databases.list()` | JSON-encoded filter object |
| `sorts` | `databases.list()` | JSON-encoded sort array |

### Node Types

| Type | Description |
|------|-------------|
| `section` | Organizational folder |
| `page` | Document with rich-text content |
| `channel` | Communication channel |
| `message` | Message within a channel |
| `database` | Structured record container |
| `record` | Individual database entry |

### Typical Response Envelope

Successful list responses follow this pattern:

```
{
  "data": [ ... ],
  "total": 42,
  "hasMore": true
}
```

Single resource responses return the resource object directly.

### Authentication Notes

- The Hoody SDK handles authentication automatically via the token provided at initialization.
- The Hoody proxy layer validates tokens before forwarding requests to the notes service.
- Do not send auth headers manually — the SDK manages this.
- Each service instance is isolated per project and container.

### Error Codes

| Code | Meaning |
|------|---------|
| 400 | Invalid request body or parameters |
| 401 | Missing or invalid authentication |
| 403 | Insufficient permissions for this operation |
| 404 | Resource not found |
| 409 | Conflict (e.g., duplicate resource) |
| 422 | Validation error on request fields |
| 500 | Internal server error |
```