NoteStream API
Base URL: https://api.notestream.cloud
All paths below are relative to this base URL. For example, POST /search means
POST https://api.notestream.cloud/search.
There is no separate AI namespace — agents call the same endpoints as the app. Every endpoint accepts either a user API token or a session JWT:
X-API-Key: your-api-key-here
Authorization: Bearer <jwt>
API keys can be created from Settings > API Keys in the NoteStream app. A token maps to a real user, so a request sees exactly what that user can see (their own notes plus everything shared with them via direct shares, hashtags, groups, and share links).
A note and its content are separate entities with a 1:1
mapping. A note holds metadata (ID, author, sharing, timestamps); note content holds the
document body as ProseMirror JSON. Every note has exactly one content record, and they share
the same ID. The root node is always { type: "doc", content: [...] }.
Search notes
Search across every note the caller can see. The request body is a filter AST; the only
required field is visibility.
POST /search
Request body
{
"ast": {
"visibility": "my-world",
"text": "meeting"
},
"page": 1,
"pageSize": 25
}
| Field | Type | Required | Description |
|---|---|---|---|
ast |
object | Yes | Filter AST (see below). |
page |
number | No | Page number (starts at 1). Defaults to 1. |
pageSize |
number | No | Results per page. Defaults to 25, max 100. |
shareCodes |
string[] | No | Share-link codes the caller has opened, to include link-shared notes. Max 20. |
Filter AST fields:
| Field | Type | Required | Description |
|---|---|---|---|
visibility |
string | Yes |
One of "public", "shared-with-me",
"all", "my-world".
"my-world" = the caller's own notes plus notes shared with
them.
|
text |
string | No | Case-insensitive substring match on note text. |
author |
object | No |
{ "op": "me" }, or
{ "op": "is", "ids":
["<email-or-userId>"] }.
|
hashtags |
array | No | [{ "hashtag": "roadmap" }]. |
mentions |
array | No | [{ "handle": "john" }]. |
sortBy |
string | No |
"newest", "oldest",
"streamOrder", "lastUpdated",
"relevance".
|
Response
{
"notes": [
{
"id": "0192d4e5-7a8b-7c6d-9e0f-1a2b3c4d5e6f",
"author": {
"id": "user-uuid",
"firstName": "Ada",
"lastName": "Lovelace",
"email": "ada@example.com",
"profilePicture": null
}
}
],
"totalCount": 250,
"page": 1,
"pageSize": 25
}
Results are note IDs (with author info), not snippets — fetch a note's body with
GET /content/:id.
Errors
| Status | Description |
|---|---|
| 401 | Missing or invalid credentials |
| 422 | Invalid request body / filter AST |
Get a note
Retrieve the metadata of a note the caller owns.
GET /notes/:id
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id |
path | string | The note ID (UUID) |
Response
{
"id": "0192d4e5-7a8b-7c6d-9e0f-1a2b3c4d5e6f",
"authorId": "user-uuid",
"isShared": false,
"pinnedPosition": null,
"folderId": null,
"deletedAt": null,
"createdAt": 1711900800000,
"updatedAt": 1711900800000
}
Errors
| Status | Description |
|---|---|
| 401 | Missing or invalid credentials |
| 404 | Note not found or not owned by the caller |
Create a note
Create a new note from ProseMirror JSON content. The note is assigned to the caller.
POST /notes
Request body
{
"content": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "A brand new note" }]
}
]
},
"isShared": true,
"anonymousRole": "VIEWER"
}
| Field | Type | Required | Description |
|---|---|---|---|
content |
object | Yes | ProseMirror document with type: "doc". |
isShared |
boolean | No |
Share the note with anonymous link access on creation. Defaults to false.
|
anonymousRole |
string | No |
"VIEWER" or "EDITOR". Role for anyone
with the link. Defaults to "VIEWER".
|
groupId |
string | No |
Create the note directly inside this group. Requires CONTRIBUTOR or
higher membership in the group (else 403).
|
pinnedPosition |
string | No | Fractional-index position to pin the note. Omit to leave unpinned. |
Response
Returns the full note object (same shape as GET /notes/:id).
Errors
| Status | Description |
|---|---|
| 401 | Missing or invalid credentials |
| 403 | groupId given but you lack posting permission in that group |
| 422 | Invalid ProseMirror JSON structure |
Read note content
Retrieve a note's content as ProseMirror JSON.
GET /content/:id
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id |
path | string | The note ID (UUID) |
Response
{
"content": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Hello world" }]
}
]
}
}
If the note has no content, returns an empty document:
{ "content": { "type": "doc", "content": [{ "type": "paragraph" }] } }
Errors
| Status | Description |
|---|---|
| 401 | Missing or invalid credentials |
| 404 | Note not found or not owned by the caller |
Replace note content
Replace the entire content of an existing note with new ProseMirror JSON.
PUT /content/:id
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id |
path | string | The note ID (UUID) |
Request body
{
"content": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Updated content" }]
}
]
}
}
The content field must be a valid ProseMirror document with
type: "doc" and a content array.
Response
{ "success": true }
Errors
| Status | Description |
|---|---|
| 401 | Missing or invalid credentials |
| 404 | Note not found or not owned by the caller |
| 422 | Invalid ProseMirror JSON structure |
Update note sharing
Set a note's sharing entries. Supports anonymous (anyone-with-link) and per-email sharing.
PUT /notes/sharing/:id
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id |
path | string | The note ID (UUID) |
Request body
{
"entries": [
{ "principalType": "ANONYMOUS", "principal": "anonymous", "role": "VIEWER" }
]
}
| Field | Type | Required | Description |
|---|---|---|---|
entries |
array | Yes | The full set of sharing entries for the note. |
entries[].principalType |
string | Yes |
"ANONYMOUS" (anyone with the link) or
"EMAIL".
|
entries[].principal |
string | Yes |
"anonymous" for ANONYMOUS, otherwise the member
email.
|
entries[].role |
string | Yes | "VIEWER" or "EDITOR". |
isPublishedToGlobalStream |
boolean | No | Publish the note to the public global stream. |
Errors
| Status | Description |
|---|---|
| 401 | Missing or invalid credentials |
| 404 | Note not found or not owned by the caller |
| 422 | Invalid request body |
Note: When a note is shared anonymously it becomes accessible via a
shareable link. The URL removes dashes from the note ID:
https://notestream.cloud/shared/note/{noteId without dashes}. For example, note
0192d4e5-7a8b-7c6d-9e0f-1a2b3c4d5e6f becomes
https://notestream.cloud/shared/note/0192d4e57a8b7c6d9e0f1a2b3c4d5e6f.
Create a group
Create a new group. The caller becomes its OWNER member.
POST /groups
Request body
{
"name": "Design Team",
"description": "Where the design crew collaborates"
}
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Group name (1–120 chars). |
description |
string | No | Optional description (max 2000 chars). |
Response
Returns the created group:
{
"group": {
"id": "0192d4e5-7a8b-7c6d-9e0f-1a2b3c4d5e6f",
"slug": "design-team-a1b2c3d4",
"name": "Design Team",
"description": "Where the design crew collaborates",
"logo": null,
"ownerId": "user-uuid",
"shareCode": null,
"shareCanEdit": false,
"createdAt": "2026-06-27T16:00:00.000Z",
"updatedAt": "2026-06-27T16:00:00.000Z"
}
}
Errors
| Status | Description |
|---|---|
| 401 | Missing or invalid credentials |
| 422 | Invalid request body |
List groups
List the groups the caller belongs to, each with their role and member count.
GET /groups
Response
{
"groups": [
{
"id": "0192d4e5-7a8b-7c6d-9e0f-1a2b3c4d5e6f",
"slug": "design-team-a1b2c3d4",
"name": "Design Team",
"description": null,
"logo": null,
"ownerId": "user-uuid",
"shareCode": null,
"shareCanEdit": false,
"createdAt": "2026-06-27T16:00:00.000Z",
"updatedAt": "2026-06-27T16:00:00.000Z",
"myRole": "OWNER",
"memberCount": 3
}
]
}
Errors
| Status | Description |
|---|---|
| 401 | Missing or invalid credentials |
Add group members
Add or update members of a group by email. Owner-only — the caller must own the group. Membership is keyed by email, so an invitee who has not registered yet resolves to their account once they sign up with that email.
POST /groups/:id/members
Parameters
| Parameter | Type | Description |
|---|---|---|
id |
string | The group's id (from create / list). |
Request body
{
"members": [
{ "email": "alice@example.com", "role": "CONTRIBUTOR" },
{ "email": "bob@example.com", "role": "VIEWER" }
]
}
| Field | Type | Required | Description |
|---|---|---|---|
members |
array | Yes | 1–100 entries. |
members[].email |
string | Yes | Member email. |
members[].role |
string | Yes |
One of OWNER, EDITOR, CONTRIBUTOR,
VIEWER.
|
Response
Returns the upserted member rows:
{
"members": [
{
"id": "0192d4e5-7a8b-7c6d-9e0f-1a2b3c4d5e6f",
"groupId": "0192d4e5-7a8b-7c6d-9e0f-1a2b3c4d5e70",
"principal": "alice@example.com",
"role": "CONTRIBUTOR",
"createdAt": "2026-06-27T16:00:00.000Z",
"updatedAt": "2026-06-27T16:00:00.000Z"
}
]
}
Errors
| Status | Description |
|---|---|
| 401 | Missing or invalid credentials |
| 404 | Group not found, or you are not its owner |
| 422 | Invalid request body |
ProseMirror node types
Notes support the following node types in the content array:
| Node type | Description | Example |
|---|---|---|
paragraph |
A block of text |
{ "type": "paragraph", "content": [{
"type": "text", "text": "..." }] }
|
heading |
Heading (attrs: level 1-6) |
{ "type": "heading", "attrs": { "level": 1
}, "content": [...] }
|
bulletList |
Unordered list containing listItem nodes |
|
orderedList |
Ordered list containing listItem nodes |
|
listItem |
List item wrapping paragraph or nested list | |
taskList |
Todo list containing taskItem nodes |
|
taskItem |
Todo item (attrs: checked boolean) |
{ "type": "taskItem", "attrs": { "checked":
false }, "content": [...] }
|
image |
Image (attrs: src) |
{ "type": "image", "attrs": { "src":
"https://..." } }
|
audio |
Audio recording (attrs: src, audioId) |
|
relation |
Link to another note (attrs: id, label) |
{ "type": "relation", "attrs": { "id":
"note-uuid", "label": "My Note" } }
|
mention |
Inline @mention (attrs: id, label, type) |
{ "type": "mention", "attrs": { "id":
"entity-name", "label": "John" } }
|
Mentions are atomic inline nodes rendered as pills (e.g. @John). The
id serves as the mention identifier and label is the display text.
The type attribute defaults to "text" for free-text
mentions.
Text marks
Text nodes can have marks for inline formatting:
| Mark type | Description |
|---|---|
bold |
Bold text |
italic |
Italic text |
strike |
Strikethrough |
underline |
Underline |
link |
Hyperlink (attrs: href) |
hashtagMark |
Hashtag (the text content includes the # prefix) |
Example: rich note
{
"content": {
"type": "doc",
"content": [
{
"type": "heading",
"attrs": { "level": 1 },
"content": [{ "type": "text", "text": "Meeting Notes" }]
},
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "Discussed the " },
{ "type": "text", "text": "roadmap", "marks": [{ "type": "bold" }] },
{ "type": "text", "text": " for Q2." }
]
},
{
"type": "taskList",
"content": [
{
"type": "taskItem",
"attrs": { "checked": false },
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Follow up with design team" }]
}
]
}
]
}
]
}
}