Notes: Real-time & Sync
Section titled “Notes: Real-time & Sync”The Notes real-time APIs drive live collaboration inside a Hoody notebook container. Use these endpoints to identify the current user, open a WebSocket channel for receiving live events, and synchronize batches of client-side mutations back to the server.
All operations on this page run against the Notes container for your project:
https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com
Identity
Section titled “Identity”Get current identity
Section titled “Get current identity”GET /api/v1/notes/me
Returns the identity this request resolved to: userId, username, role, and notebookId. role is always the role stored for that username in that notebook, never the one the request carries: a later call with another role value (query parameter or Bearer token) does not change it, and it is none when the notebook has no such user. The identity comes from an Authorization: Bearer token (base64url JSON of {userId, notebookId, username, role}), or from the username and role query parameters (defaults user / owner). Query identities are auto-provisioned in the default notebook on first use, with the role of that first call. A user first seen with role none is not a member.
This endpoint takes no parameters.
curl -X GET "https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/me" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.notes.whoami();{ "userId": "67e89abc123def456789abcd", "username": "alice", "role": "owner", "notebookId": "890abcdef12345678901cdef"}{ "message": "Invalid request", "code": "BAD_REQUEST", "details": [ { "path": "/", "message": "Request could not be processed" } ]}{ "message": "Unauthorized", "code": "UNAUTHORIZED", "details": [ { "path": "/authorization", "message": "Missing or invalid credentials" } ]}WebSocket lifecycle
Section titled “WebSocket lifecycle”A live collaboration channel requires two steps: initialize a socket session to obtain a socket ID, then open the WebSocket using that ID.
Initialize a WebSocket session
Section titled “Initialize a WebSocket session”POST /api/v1/notes/sockets
Creates a new socket session and returns the socket ID needed to open the WebSocket.
This endpoint takes no parameters.
curl -X POST "https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/sockets" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.notes.sockets.create();{ "id": "9c3f1e7a4b2d8f6e5a0c1d2e"}{ "message": "Invalid request", "code": "BAD_REQUEST", "details": [ { "path": "/", "message": "Request body must be empty" } ]}{ "message": "Internal server error", "code": "INTERNAL_SERVER_ERROR"}Open a WebSocket connection
Section titled “Open a WebSocket connection”GET /api/v1/notes/sockets/{socketId}
Upgrades the HTTP connection to a WebSocket using a previously initialized socket ID. Once upgraded, send and receive messages as text frames.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
socketId | path | string | Yes | The socket ID returned by POST /api/v1/notes/sockets |
curl -i -X GET "https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/sockets/{socketId}" \ -H "Authorization: Bearer <token>" \ -H "Connection: Upgrade" \ -H "Upgrade: websocket"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.notes.sockets.connect(socketId);A 101 Switching Protocols response carries no body. Once the WebSocket is open, the client and server exchange JSON text frames carrying synchronizer.input, synchronizer.output, and notebook, user, and comment change events.
{ "message": "Socket not found", "code": "SOCKET_NOT_FOUND", "details": [ { "path": "/socketId", "message": "No active session matches the provided socketId" } ]}{ "message": "WebSocket upgrade failed", "code": "UPGRADE_FAILED"}The full WebSocket URL to connect to from a browser or Node.js client is:
wss://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/sockets/{socketId}
Mutations
Section titled “Mutations”Sync client mutations
Section titled “Sync client mutations”POST /api/v1/notes/notebooks/{notebookId}/mutations
Processes a batch of client-side mutations (node CRUD, reactions, interactions, document updates) and returns a per-mutation status result for each entry.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
notebookId | path | string | Yes | The notebook receiving the mutations |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
mutations | array | Yes | Ordered list of mutations to apply. Maximum 500 entries per request. Each entry follows the envelope shape below. |
Each entry in mutations carries the following envelope fields:
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Client-generated mutation ID, echoed back in the response |
createdAt | string | Yes | Client-side ISO 8601 timestamp for the mutation |
type | string | Yes | Discriminator selecting the payload shape (see below) |
data | object | Yes | Payload matching the selected type |
The supported type values and their required data fields are:
node.create
| Field | Type | Required | Description |
|---|---|---|---|
data.nodeId | string | Yes | Identifier of the new node |
data.updateId | string | Yes | Client-generated update ID |
data.createdAt | string | Yes | ISO 8601 creation timestamp |
data.data | string | Yes | Encoded node payload |
node.update
| Field | Type | Required | Description |
|---|---|---|---|
data.nodeId | string | Yes | Identifier of the node being updated |
data.updateId | string | Yes | Client-generated update ID |
data.data | string | Yes | Encoded update payload |
data.createdAt | string | Yes | ISO 8601 update timestamp |
node.delete
| Field | Type | Required | Description |
|---|---|---|---|
data.nodeId | string | Yes | Identifier of the node being deleted |
data.rootId | string | Yes | Root node ID containing the target |
data.deletedAt | string | Yes | ISO 8601 deletion timestamp |
node.reaction.create
| Field | Type | Required | Description |
|---|---|---|---|
data.nodeId | string | Yes | Node receiving the reaction |
data.reaction | string | Yes | Reaction identifier (for example, a short code) |
data.rootId | string | Yes | Root node ID containing the target |
data.createdAt | string | Yes | ISO 8601 timestamp of the reaction |
node.reaction.delete
| Field | Type | Required | Description |
|---|---|---|---|
data.nodeId | string | Yes | Node whose reaction is being removed |
data.reaction | string | Yes | Reaction identifier being removed |
data.rootId | string | Yes | Root node ID containing the target |
data.deletedAt | string | Yes | ISO 8601 timestamp of the removal |
node.interaction.seen
| Field | Type | Required | Description |
|---|---|---|---|
data.nodeId | string | Yes | Node being marked as seen |
data.collaboratorId | string | Yes | Collaborator who saw the node |
data.seenAt | string | Yes | ISO 8601 timestamp of the seen event |
node.interaction.opened
| Field | Type | Required | Description |
|---|---|---|---|
data.nodeId | string | Yes | Node being marked as opened |
data.collaboratorId | string | Yes | Collaborator who opened the node |
data.openedAt | string | Yes | ISO 8601 timestamp of the opened event |
document.update
| Field | Type | Required | Description |
|---|---|---|---|
data.documentId | string | Yes | Identifier of the document being updated |
data.updateId | string | Yes | Client-generated update ID |
data.data | string | Yes | Encoded document update payload |
data.createdAt | string | Yes | ISO 8601 update timestamp |
curl -X POST "https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/notebooks/{notebookId}/mutations" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "mutations": [ { "id": "m-9f3a1b", "createdAt": "2026-01-15T12:00:00.000Z", "type": "node.create", "data": { "nodeId": "n-aaaa1111bbbb2222cccc3333", "updateId": "u-7c1d2e", "createdAt": "2026-01-15T12:00:00.000Z", "data": "eyJ0ZXh0IjoiSGVsbG8sIHdvcmxkISJ9" } }, { "id": "m-7b2c4d", "createdAt": "2026-01-15T12:00:05.000Z", "type": "node.reaction.create", "data": { "nodeId": "n-aaaa1111bbbb2222cccc3333", "reaction": ":wave:", "rootId": "n-root1111aaaa2222bbbb3333", "createdAt": "2026-01-15T12:00:05.000Z" } } ] }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.notes.mutations.sync(notebookId, { mutations: [ { id: 'm-9f3a1b', createdAt: '2026-01-15T12:00:00.000Z', type: 'node.create', data: { nodeId: 'n-aaaa1111bbbb2222cccc3333', updateId: 'u-7c1d2e', createdAt: '2026-01-15T12:00:00.000Z', data: 'eyJ0ZXh0IjoiSGVsbG8sIHdvcmxkISJ9', }, }, { id: 'm-7b2c4d', createdAt: '2026-01-15T12:00:05.000Z', type: 'node.reaction.create', data: { nodeId: 'n-aaaa1111bbbb2222cccc3333', reaction: ':wave:', rootId: 'n-root1111aaaa2222bbbb3333', createdAt: '2026-01-15T12:00:05.000Z', }, }, ],});On a successful call, the response carries the per-mutation status results for the batch, with one entry for each submitted mutation’s id indicating how the server processed it.
| 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 |
| 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_no_access | No access to notebook | The notebook exists but the caller is not a member, or it is not open | Verify user identity or request access from the owner |