v7.6.0

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,
  "sourcePlatform": null,
  "sourceLink": 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.
sourcePlatform string No Where the note came from, e.g. "X". Rendered as a "via X ↗" chip. Max 64 chars.
sourceLink string No Permalink for the source chip. Must be an http(s) URL, max 2048 chars.

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.


Update note source

Set or clear a note's source-provenance metadata, without touching its content. The app renders these as a "via <sourcePlatform> ↗" chip linking to sourceLink.

PATCH /notes/:id/source

Parameters

Name In Type Description
id path string The note ID (UUID)

Request body

{
  "sourcePlatform": "X",
  "sourceLink": "https://x.com/jack/status/20"
}
Field Type Required Description
sourcePlatform string | null No Platform label, max 64 chars. null clears it.
sourceLink string | null No Permalink, must be an http(s) URL, max 2048 chars. null clears it.

At least one of the two fields must be present. An omitted field is left unchanged.

Response

Returns the full note object (same shape as GET /notes/:id).

Errors

Status Description
400 Invalid payload, or neither field provided
401 Missing or invalid credentials
404 Note not found or not owned by the caller

Import saved items

Ingest a batch of saved/bookmarked items from an external platform as notes. The caller fetches the items itself (Notestream never receives platform credentials) and POSTs the normalized batch here; the server owns dedupe, note construction, and the resume cursor.

POST /imports/:platform
GET  /imports/:platform

Parameters

Name In Type Description
platform path string x or instagram

Request body (POST)

{
  "sourceId": "x-session",
  "cursor": "opaque-resume-token",
  "items": [
    {
      "externalId": "20",
      "url": "https://x.com/jack/status/20",
      "text": "just setting up my twttr",
      "authorHandle": "jack",
      "authorName": "jack",
      "createdAt": 1143843780000,
      "collections": ["Trip Ideas"],
      "mediaUrls": []
    }
  ]
}
Field Type Required Description
sourceId string Yes Which adapter produced the batch: x-session, x-api, instagram-session, or instagram-dyi. Max 64 chars.
items array Yes Up to 200 normalized items.
cursor string | null No Resume token to persist after this batch. Omit to leave the stored cursor untouched; null clears it and forces a full re-read next run. Max 4096 chars.
items[].externalId string Yes Platform-native id. This is the dedupe key, so re-POSTing a batch is a safe no-op. Max 256 chars.
items[].url string Yes Canonical permalink (http(s), max 2048 chars). Stored as the note's sourceLink.
items[].text string Yes Item body (tweet text or caption). May be empty; max 40,000 chars.
items[].authorHandle string No Author handle without a leading @. Max 128 chars.
items[].authorName string No Author display name. Max 256 chars.
items[].createdAt number No When the item was created on the platform (epoch ms). Accepted but not yet applied to the note's timestamps.
items[].collections array<string> No Up to 20 collection/folder names. Each becomes a slugified hashtag on a new note, or is merged into an already-imported note when missing ("Trip Ideas 🇯🇵"#trip-ideas).
items[].mediaUrls array<string> No Up to 10 media URLs. Accepted and validated but not yet rendered in the note.

Each new imported item becomes a note containing the item text, an attribution line with the permalink, and the collection hashtags, with sourcePlatform / sourceLink set for the source chip. If an item is already in the import ledger and the request supplies new collections, the server appends missing collection hashtags to the existing note through the collab document path instead of creating a duplicate.

Response (POST)

{
  "imported": 1,
  "skipped": 0,
  "updated": 0,
  "noteIds": ["0192d4e5-7a8b-7c6d-9e0f-1a2b3c4d5e6f"]
}

updated counts already-imported notes that gained collection hashtags. skipped counts items already present in this user's import ledger for the platform with nothing to merge. noteIds lists the notes created, in item order.

Response (GET)

{
  "platform": "x",
  "sourceId": "x-session",
  "cursor": "opaque-resume-token",
  "lastSyncedAt": 1711900800000,
  "importedCount": 412
}

sourceId, cursor, and lastSyncedAt are null until the first successful ingest.

Errors

Status Description
400 Invalid import payload
401 Missing or invalid credentials
404 Unknown import platform

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" }]
              }
            ]
          }
        ]
      }
    ]
  }
}

Webhooks

Webhook subscriptions notify external systems when note content changes and the note's plain text contains the configured trigger text. The default trigger text is @agent.

All webhook management endpoints accept either a JWT (Authorization: Bearer <jwt>) or a user API token (X-API-Key: <key>).

List webhooks

GET /webhooks

Response:

{
  "subscriptions": [
    {
      "id": "0192d4e5-7a8b-7c6d-9e0f-1a2b3c4d5e6f",
      "name": "Agent runner",
      "url": "https://example.com/notestream",
      "hasSecret": true,
      "triggerText": "@agent",
      "enabled": true,
      "lastDeliveredAt": 1711900800000,
      "lastDeliveryStatus": 200,
      "lastDeliveryError": null,
      "createdAt": 1711900800000,
      "updatedAt": 1711900800000
    }
  ]
}

Secrets are never returned. Use hasSecret to tell whether a subscription has one.

Create a webhook

POST /webhooks

Request body:

{
  "url": "https://example.com/notestream",
  "name": "Agent runner",
  "secret": "shared-signing-secret",
  "triggerText": "@agent",
  "enabled": true
}
Field Type Required Description
url string Yes Delivery URL. Must use http or https.
name string No Human-readable name.
secret string No Shared HMAC secret, 8 to 512 characters. Never returned by the API.
triggerText string No Text that must appear in the note. Defaults to @agent.
enabled boolean No Whether to deliver events. Defaults to true.

Production rejects localhost, loopback, link-local, and private literal IP URLs. Localhost and loopback URLs are accepted only outside production when ALLOW_LOCAL_WEBHOOK_URLS=true.

Response status: 201 Created, with the subscription object shown above.

Delete a webhook

DELETE /webhooks/:id

Response status: 204 No Content.

Delivery payload

When note content changes through collaboration, REST content sync, or AI note/content endpoints, NoteStream sends:

{
  "eventId": "0192d4e5-7a8b-7c6d-9e0f-1a2b3c4d5e6f",
  "event": "note.content.updated",
  "noteId": "note-uuid",
  "userId": "note-owner-user-uuid",
  "source": "collab",
  "triggerText": "@agent",
  "subscriptionId": "subscription-uuid",
  "idempotencyKey": "stable-sha256-key",
  "contentHash": "sha256-of-plain-text",
  "plainText": "@agent please summarize this note",
  "occurredAt": "2026-06-20T10:30:00.000Z"
}

source is one of collab, content-push, content-bulk-push, ai-content, or ai-note. The same subscription, note, trigger text, and content hash produce the same idempotencyKey, so unchanged matching content is delivered once.

Delivery headers

Every delivery includes:

Header Description
X-NoteStream-Event Event name, currently note.content.updated.
X-NoteStream-Delivery Delivery/event ID.
X-NoteStream-Idempotency-Key Stable key for duplicate suppression.
X-NoteStream-Subscription Subscription ID.
X-NoteStream-Note-Id Note ID.
X-NoteStream-Content-Hash SHA-256 hash used to detect unchanged content.
X-NoteStream-Occurred-At ISO timestamp for the created delivery event.

When a subscription has a secret, deliveries also include:

X-NoteStream-Signature-256: sha256=<hex hmac>

Verify it by computing HMAC-SHA256 over the raw JSON request body with the subscription secret and comparing the hex digest.

Non-2xx responses and network errors are recorded as delivery failures on the subscription's lastDeliveryStatus and lastDeliveryError fields.