Notes: File Uploads
Section titled “Notes: File Uploads”The Notes service stores files and avatar images attached to notebooks. This page covers the file management endpoints: uploading and downloading avatars, listing and downloading notebook files, and the TUS 1.0.0 protocol for resumable file uploads.
Avatars
Section titled “Avatars”Avatars are user profile images. Upload a raw image (JPEG, PNG, or WebP, up to 10 MB) as the request body. The server resizes the image to fit within 500x500 pixels and stores it as JPEG. Use the returned id to read the avatar back.
POST /api/v1/notes/avatars
Section titled “POST /api/v1/notes/avatars”Uploads one raw image (JPEG, PNG, or WebP, at most 10 MB) as the request body, not multipart. Set Content-Type to the image type. The image is resized to fit 500x500 and stored as JPEG; the returned id is what the avatar download reads.
This endpoint takes no parameters.
Request body
Section titled “Request body”The body is the raw image bytes. Set Content-Type to one of:
image/jpegimage/pngimage/webp
The body must be a decodable image no larger than 10 MB, and the decoded image must have no more than 50 million pixels.
Responses
Section titled “Responses”The avatar was stored and is ready to read.
{ "success": true, "id": "67e89abc123def456789abcf"}The body is empty, the image could not be decoded, or it has more than 50 million pixels.
{ "code": "avatar_file_not_uploaded", "message": "No avatar file was uploaded."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
avatar_file_not_uploaded | Invalid upload | The body is empty, the image could not be decoded, or it has more than 50 million pixels | Send the raw image bytes with Content-Type image/jpeg, image/png, or image/webp |
The image could not be processed or stored, or the server runtime has no image processing.
{ "code": "avatar_upload_failed", "message": "Failed to upload avatar."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
avatar_upload_failed | Upload failed | Avatar upload failed due to a server error: the image could not be processed or stored, or the server runtime has no image processing (Bun.Image) | Retry the upload; if it keeps failing for valid images, the server cannot process images (report it) |
The server is processing as many avatar uploads as it allows and its wait queue is full.
{ "code": "avatar_processing_busy", "message": "Too many avatar uploads are being processed. Retry shortly."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
avatar_processing_busy | Processing queue full | The server is processing as many avatar uploads as it allows and its wait queue is full | Retry after the number of seconds in the Retry-After header |
The response also carries a Retry-After header with the number of seconds to wait before retrying.
Example
Section titled “Example”curl -X POST "https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/avatars" \ -H "Content-Type: image/jpeg" \ --data-binary @avatar.jpgimport { HoodyClient } from 'hoody-sdk';import { readFileSync } from 'node:fs';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const bytes = readFileSync('avatar.jpg');const { data } = await client.notes.avatars.upload(bytes);console.log(data.id);GET /api/v1/notes/avatars/{avatarId}
Section titled “GET /api/v1/notes/avatars/{avatarId}”Returns the avatar image as JPEG binary data.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
avatarId | path | string | Yes | The id returned when the avatar was uploaded |
Responses
Section titled “Responses”The body is the avatar image in JPEG format (binary bytes). Save the response to a file or feed it to an image decoder.
{ "code": "avatar_not_found", "message": "Avatar not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
avatar_not_found | Avatar not found | No avatar exists with this ID | Verify the avatar ID; the avatar may need to be re-uploaded |
{ "code": "avatar_download_failed", "message": "Failed to download avatar."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
avatar_download_failed | Download failed | Avatar image could not be read from storage | Retry; if it persists, storage is unavailable |
Example
Section titled “Example”curl "https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/avatars/{avatarId}" -o avatar.jpgimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const blob = await client.notes.avatars.download(avatarId);Files are attached to notebooks as file nodes. The list endpoint enumerates every file in a notebook; the download endpoint returns a file’s raw bytes. Bytes are uploaded through the TUS resumable protocol covered in the next section.
GET /api/v1/notes/notebooks/{notebookId}/files
Section titled “GET /api/v1/notes/notebooks/{notebookId}/files”Returns a paginated list of files uploaded to a notebook. Supports limit/offset pagination.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
notebookId | path | string | Yes | The id of the notebook |
limit | query | integer | No | Maximum number of files to return. Default: 50 |
offset | query | integer | No | Number of files to skip. Default: 0 |
Responses
Section titled “Responses”{ "files": [ { "id": "67e89abc123def456789ab01", "name": "q1-report.pdf", "mimeType": "application/pdf", "size": 1048576, "createdAt": "2024-01-15T10:30:00.000Z", "createdBy": "alice", "documentId": "67e89abc123def456789ab02", "documentName": "Q1 Report" }, { "id": "67e89abc123def456789ab03", "name": "chart.png", "mimeType": "image/png", "size": 524288, "createdAt": "2024-01-16T14:22:00.000Z", "createdBy": "bob", "documentId": "67e89abc123def456789ab04", "documentName": null } ], "total": 2}{ "code": "notebook_no_access", "message": "You do not have access to this notebook."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
notebook_not_found | Notebook not found | No notebook exists with the provided ID | Verify the notebook ID by listing your notebooks |
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 |
{ "code": "notebook_not_found", "message": "Notebook not found."}| 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 |
Example
Section titled “Example”curl "https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/notebooks/{notebookId}/files?limit=50&offset=0"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
for await (const file of client.notes.files.listIterator(notebookId, { limit: 100, offset: 0 })) { console.log(file.name, file.size, file.mimeType);}GET /api/v1/notes/notebooks/{notebookId}/files/{fileId}
Section titled “GET /api/v1/notes/notebooks/{notebookId}/files/{fileId}”Downloads the content of an uploaded file byte for byte. The response content type is the uploaded one for images, audio, video, PDF, and plain text (sent inline). Every other type, including HTML, SVG, and XML, is sent as application/octet-stream with Content-Disposition: attachment. Every response carries X-Content-Type-Options: nosniff and a sandboxing Content-Security-Policy header.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
notebookId | path | string | Yes | The id of the notebook |
fileId | path | string | Yes | The id of the file |
Responses
Section titled “Responses”The body is the file’s raw bytes. The response content type is the uploaded one for images, audio, video, PDF, and plain text; every other type is returned as application/octet-stream with Content-Disposition: attachment. Every response carries X-Content-Type-Options: nosniff and a sandboxing Content-Security-Policy header.
The file does not exist, is not a file node, or has not finished uploading.
{ "code": "file_not_found", "message": "File not found."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
file_not_found | File not found or not ready | The file does not exist, is not a file node, or has not finished uploading | Verify the file ID by listing the notebook’s files and ensure the file upload is complete |
{ "code": "file_no_access", "message": "You do not have access to this file."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
file_no_access | No access to file | User does not have permission to download this file | Request access from the node collaborators |
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 |
{ "code": "file_not_found", "message": "File not found in storage."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
file_not_found | File missing from storage | The file record exists but the file data is missing from storage | The file may need to be re-uploaded |
notebook_not_found | Notebook not found | No notebook exists with the provided ID | Verify the notebook ID in the URL |
Example
Section titled “Example”curl "https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/notebooks/{notebookId}/files/{fileId}" -o file.datimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const blob = await client.notes.files.download(notebookId, fileId);Resumable uploads (TUS)
Section titled “Resumable uploads (TUS)”Notebook file bytes are uploaded with the TUS 1.0.0 resumable upload protocol. The flow has three steps:
- Create a file node in the notebook (type
file, statuspending) through the node creation endpoint. The file node’ssizeis fixed at this point and is the size the upload must deliver. POSTto the upload URL to create the upload, declaring the total size inUpload-Length.PATCHchunks in order, starting atUpload-Offset: 0and increasing the offset after each chunk. WhenUpload-OffsetreachesUpload-Length, the file is ready.
After an interruption, HEAD the upload URL to read the current Upload-Offset and resume from there. To abandon an in-flight upload, DELETE the upload URL. Only the user who created the file node may upload to it.
Every TUS request must include the Tus-Resumable: 1.0.0 header. Anything else returns 412.
POST /api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus
Section titled “POST /api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus”Creates a TUS 1.0.0 resumable upload for a file node. Send no body. A create that already holds every byte (Upload-Length: 0, or a creation-with-upload body that carried every byte) finishes the upload at once with 200.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
notebookId | path | string | Yes | The id of the notebook |
fileId | path | string | Yes | The id of the file node |
Tus-Resumable | header | string | Yes | TUS protocol version. Must be 1.0.0; anything else returns 412 |
Upload-Length | header | integer | Yes | Total size of the file in bytes. Must equal the file node’s size (set when the node was created), which must not exceed the notebook’s maximum file size. Upload-Defer-Length is not supported |
Upload-Metadata | header | string | No | Optional TUS metadata: comma-separated key base64(value) pairs |
Responses
Section titled “Responses”The upload was created and is already complete (for example, Upload-Length: 0). The file is ready; a HEAD to the URL in the Location header reports it complete.
{ "uploadId": "67e89abc123def456789abf0"}| Header | Description |
|---|---|
Location | URL of the upload: this path, with the caller’s username and role query parameters when it sent them |
Tus-Resumable | TUS protocol version (1.0.0) |
The upload was created. Send the chunks to the URL returned in the Location header (this same path).
Invalid notebook or file id; the node is not a file; the file is already uploaded; the file node’s size exceeds the notebook’s maximum file size; Upload-Length is missing, deferred, or not equal to the file node’s size; or a TUS header is malformed.
{ "code": "file_not_found", "message": "File not found."}Notebook is read-only, the caller did not create this file node, or the caller lost access to it.
{ "code": "notebook_no_access", "message": "You do not have access to this notebook."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
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 |
File node not found in this notebook.
{ "code": "notebook_not_found", "message": "Notebook not found."}| 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 |
Missing or unsupported Tus-Resumable header. Only 1.0.0 is supported.
{ "code": "file_upload_invalid_request", "message": "Unsupported TUS protocol version."}A creation-with-upload body is longer than Upload-Length.
{ "code": "file_size_mismatch", "message": "Upload body exceeds Upload-Length."}The upload could not be recorded or stored.
{ "code": "file_upload_failed", "message": "Failed to record the upload."}Upload-Concat (parallel uploads) is not supported.
{ "code": "file_upload_unsupported", "message": "Upload-Concat is not supported."}Example
Section titled “Example”curl -X POST "https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus" \ -H "Tus-Resumable: 1.0.0" \ -H "Upload-Length: 1048576"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.files.uploads.create(notebookId, fileId, { TusResumable: '1.0.0', UploadLength: 1048576,});PATCH /api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus
Section titled “PATCH /api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus”Appends one chunk of bytes to the upload at Upload-Offset. Content-Type must be application/offset+octet-stream. Chunks for one upload are taken one at a time; a chunk sent while another is being written returns 409 once that one moves the offset.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
notebookId | path | string | Yes | The id of the notebook |
fileId | path | string | Yes | The id of the file node |
Tus-Resumable | header | string | Yes | TUS protocol version. Must be 1.0.0; anything else returns 412 |
Upload-Offset | header | integer | Yes | Offset this chunk starts at, the value the HEAD check (or the previous chunk) returned |
Request body
Section titled “Request body”The chunk bytes with Content-Type: application/offset+octet-stream.
Responses
Section titled “Responses”The final chunk was received; the file is ready.
{ "uploadId": "67e89abc123def456789abf0"}The response has no body. The chunk was stored; more bytes are expected.
| Header | Description |
|---|---|
Upload-Offset | New offset; send the next chunk from here |
Tus-Resumable | TUS protocol version (1.0.0) |
Invalid notebook or file id; the node is not a file; the upload is already finished; the chunk sends Upload-Length, which is fixed at creation; or Content-Type or Upload-Offset is malformed.
{ "code": "file_already_uploaded", "message": "File is already uploaded."}Notebook is read-only, the caller did not create this file node, the caller lost access to it, or Upload-Offset or Content-Type is missing.
{ "code": "notebook_no_access", "message": "You do not have access to this notebook."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
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 |
No upload exists for this file (create one first), or the file node was not found.
{ "code": "notebook_not_found", "message": "Notebook not found."}| 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 |
Upload-Offset does not match the bytes the server holds; send a HEAD to read the current offset.
{ "code": "file_upload_offset_conflict", "message": "Upload-Offset does not match the server offset."}Missing or unsupported Tus-Resumable header. Only 1.0.0 is supported.
{ "code": "file_upload_invalid_request", "message": "Unsupported TUS protocol version."}The chunk would run past Upload-Length.
{ "code": "file_size_mismatch", "message": "Chunk would exceed Upload-Length."}The upload could not be recorded or stored. On the final chunk the bytes are kept but the upload may not be recorded: when a HEAD reports offset == length, a PATCH at that offset with an empty body finishes it (200 with uploadId, also when the file was already marked ready), or returns 400 file_already_uploaded when the upload was recorded.
{ "code": "file_upload_failed", "message": "Failed to record the chunk."}Example
Section titled “Example”curl -X PATCH "https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus" \ -H "Tus-Resumable: 1.0.0" \ -H "Upload-Offset: 0" \ -H "Content-Type: application/offset+octet-stream" \ --data-binary @chunk-0.binimport { HoodyClient } from 'hoody-sdk';import { readFileSync } from 'node:fs';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const chunk = readFileSync('chunk-0.bin');await client.notes.files.uploads.writeChunk(notebookId, fileId, chunk, { TusResumable: '1.0.0', UploadOffset: 0,});HEAD /api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus
Section titled “HEAD /api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus”Reports how many bytes the server holds. A finished upload keeps answering, with Upload-Offset equal to Upload-Length. Use this after an interruption to resume from the correct offset.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
notebookId | path | string | Yes | The id of the notebook |
fileId | path | string | Yes | The id of the file node |
Tus-Resumable | header | string | Yes | TUS protocol version. Must be 1.0.0; anything else returns 412 |
Responses
Section titled “Responses”Upload found. The Upload-Offset header reports the bytes the server holds; an offset equal to Upload-Length means the upload is complete.
| Header | Description |
|---|---|
Upload-Offset | Bytes received so far; send the next chunk from here |
Upload-Length | Total size declared at creation |
Tus-Resumable | TUS protocol version (1.0.0) |
Cache-Control | Always no-store |
Invalid notebook or file id, or the node is not a file.
Notebook is read-only, or the caller did not create this file node or lost access to it.
{ "code": "notebook_no_access", "message": "You do not have access to this notebook."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
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 |
No upload exists for this file (create one first), or the file node was not found.
{ "code": "notebook_not_found", "message": "Notebook not found."}| 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 |
Missing or unsupported Tus-Resumable header.
The upload metadata could not be read.
Example
Section titled “Example”curl -I "https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus" \ -H "Tus-Resumable: 1.0.0"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.files.uploads.getOffset(notebookId, fileId, { TusResumable: '1.0.0',});DELETE /api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus
Section titled “DELETE /api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus”Abandons an unfinished upload and deletes the bytes received so far.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
notebookId | path | string | Yes | The id of the notebook |
fileId | path | string | Yes | The id of the file node |
Tus-Resumable | header | string | Yes | TUS protocol version. Must be 1.0.0; anything else returns 412 |
Responses
Section titled “Responses”The response has no body. The upload was aborted.
| Header | Description |
|---|---|
Tus-Resumable | TUS protocol version (1.0.0) |
Invalid ids; the node is not a file; or the upload is already finished and cannot be aborted.
{ "code": "file_already_uploaded", "message": "File is already uploaded."}Notebook is read-only, or the caller did not create this file node or lost access to it.
{ "code": "notebook_no_access", "message": "You do not have access to this notebook."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
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 |
No upload exists for this file (create one first), or the file node was not found.
{ "code": "notebook_not_found", "message": "Notebook not found."}| 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 |
Missing or unsupported Tus-Resumable header. Only 1.0.0 is supported.
{ "code": "file_upload_invalid_request", "message": "Unsupported TUS protocol version."}The upload could not be recorded or stored.
{ "code": "file_upload_failed", "message": "Failed to abort the upload."}Example
Section titled “Example”curl -X DELETE "https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/notebooks/{notebookId}/files/{fileId}/tus" \ -H "Tus-Resumable: 1.0.0"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.files.uploads.cancel(notebookId, fileId, { TusResumable: '1.0.0',});