Skip to content

Add Chat REST API endpoints#2915

Open
themightychris wants to merge 4 commits intoanyproto:developfrom
themightychris:GO-0000-add-chat-rest-api
Open

Add Chat REST API endpoints#2915
themightychris wants to merge 4 commits intoanyproto:developfrom
themightychris:GO-0000-add-chat-rest-api

Conversation

@themightychris
Copy link
Copy Markdown

@themightychris themightychris commented Feb 1, 2026

Summary

Adds REST API endpoints for chat functionality and file uploads, exposing the existing gRPC layer to the public API.

New Endpoints

Method Endpoint Description
GET /v1/spaces/{space_id}/chats/{chat_id}/messages List messages (paginated)
GET /v1/spaces/{space_id}/chats/{chat_id}/messages/{message_id} Get single message
POST /v1/spaces/{space_id}/chats/{chat_id}/messages Send message
PATCH /v1/spaces/{space_id}/chats/{chat_id}/messages/{message_id} Edit message
DELETE /v1/spaces/{space_id}/chats/{chat_id}/messages/{message_id} Delete message
POST /v1/spaces/{space_id}/chats/{chat_id}/messages/{message_id}/reactions Toggle reaction
POST /v1/spaces/{space_id}/chats/{chat_id}/search Search messages
POST /v1/spaces/{space_id}/chats/{chat_id}/read Mark messages as read
GET /v1/spaces/{space_id}/chats/{chat_id}/subscribe Real-time SSE event stream
POST /v1/spaces/{space_id}/files Upload file (multipart/form-data)

Features

  • Cursor-based pagination for message listing (before_order_id, after_order_id)
  • Message content with text styling and marks
  • File/image/link attachments
  • Emoji reactions with user tracking
  • Full-text message search
  • Rate limiting on write operations
  • Mark messages/mentions as read with race condition protection via last_state_id
  • File uploads via multipart/form-data, returns object_id for use in attachments
  • Real-time SSE subscriptions for live chat events:
    • Initial messages on connect
    • Message add/update/delete events
    • Reaction updates
    • Chat state changes (unread counts)
    • 30-second keepalive pings

Files Changed

  • core/api/model/chat.go: Chat message models
  • core/api/model/file.go: File upload response model
  • core/api/model/sse.go: SSE event models
  • core/api/service/chat.go: Chat service (messages, reactions, search, read, subscriptions)
  • core/api/service/file.go: File upload service
  • core/api/service/sse.go: SSE session management
  • core/api/handler/chat.go: Chat HTTP handlers
  • core/api/handler/file.go: File upload handler
  • core/api/server/router.go: Route registration
  • core/api/core/core.go: Extended ClientCommands interface

Usage Examples

# Mark messages as read
curl -X POST "http://localhost:31012/v1/spaces/$SPACE/chats/$CHAT/read" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"messages","before_order_id":"!!Xz","last_state_id":"..."}'

# Upload a file
curl -X POST "http://localhost:31012/v1/spaces/$SPACE/files" \
  -H "Authorization: Bearer $API_KEY" \
  -F "file=@image.png" \
  -F "type=image"
# Returns: {"object":"file","object_id":"bafyrei..."}

# Use uploaded file in chat message
curl -X POST "http://localhost:31012/v1/spaces/$SPACE/chats/$CHAT/messages" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"Check this out","attachments":[{"target":"bafyrei...","type":"image"}]}'

Test Plan

  • All chat CRUD operations work correctly
  • Pagination with cursor-based navigation
  • Reactions toggle on/off
  • Message search returns expected results
  • Mark as read updates unread counters
  • File upload returns valid object_id
  • Uploaded files can be attached to messages
  • SSE subscription streams real-time events
  • Connection cleanup on client disconnect

🤖 Generated with Claude Code

@jsandai
Copy link
Copy Markdown

jsandai commented Feb 1, 2026

Nice work on this! I've been working on chat support from the CLI side (anyproto/anytype-cli#25) and noticed you took the REST API approach. After comparing both, I think yours is architecturally cleaner and more consistent with how the CLI is designed to work.

One feature we implemented that might be worth adding here: real-time subscriptions. Our CLI has a chat subscribe command that uses gRPC streaming to get live events (new messages, edits, reactions, etc.) as they happen. This is essential for building chat bots or bridges that need to react to messages without polling.

Would you be open to adding a subscription endpoint? Two options that would fit the REST API pattern:

WebSocket:

GET /v1/spaces/{space_id}/chats/{chat_id}/subscribe
Upgrade: websocket

Or SSE (simpler):

GET /v1/spaces/{space_id}/chats/{chat_id}/events
Accept: text/event-stream

Either would wrap the existing ChatSubscribe gRPC stream internally. Happy to contribute the implementation if you'd like — just let me know which approach you'd prefer.

@themightychris
Copy link
Copy Markdown
Author

One feature we implemented that might be worth adding here: real-time subscriptions. Our CLI has a chat subscribe command that uses gRPC streaming to get live events (new messages, edits, reactions, etc.) as they happen. This is essential for building chat bots or bridges that need to react to messages without polling.

Would you be open to adding a subscription endpoint?

happy to take a look into it!

is that subscribe command up somewhere I could take a look at it?

@jsandai
Copy link
Copy Markdown

jsandai commented Feb 1, 2026

Awesome! Here's our implementation:

CLI subscribe command: anyproto/anytype-cli#25

Key files:

The flow:

  1. Call ChatSubscribeLastMessages → get subscription ID + initial messages
  2. Listen to ListenSessionEvents stream
  3. Filter events by subscription ID (check SubIds field)
  4. Handle event types: ChatAdd, ChatUpdate, ChatDelete, ChatUpdateReactions, ChatUpdateMessageReadStatus

For the REST API, we were thinking SSE would fit well:

GET /v1/spaces/{space_id}/chats/{chat_id}/subscribe
Accept: text/event-stream

Happy to collaborate on the implementation!

@themightychris themightychris force-pushed the GO-0000-add-chat-rest-api branch from 9624401 to b01b7f4 Compare February 2, 2026 04:36
themightychris added a commit to themightychris/anytype-heart that referenced this pull request Feb 2, 2026
@themightychris
Copy link
Copy Markdown
Author

Awesome! Here's our implementation:

CLI subscribe command: anyproto/anytype-cli#25

Key files:

I'm not seeing any of that subscribe stuff in anyproto/anytype-cli#25, is there a commit not pushed up?

@themightychris themightychris force-pushed the GO-0000-add-chat-rest-api branch from b01b7f4 to 0ab42a2 Compare February 2, 2026 05:03
themightychris added a commit to themightychris/anytype-heart that referenced this pull request Feb 2, 2026
themightychris added a commit to themightychris/anytype-heart that referenced this pull request Feb 2, 2026
@themightychris
Copy link
Copy Markdown
Author

@jsandai had a go at it, seems to work!

@jsandai
Copy link
Copy Markdown

jsandai commented Feb 2, 2026

Hey Chris! This REST API is fantastic — exactly what's needed for programmatic chat integration. I've been testing it against a self-hosted network and everything works great: messages, reactions, SSE subscriptions.

For chat bot use cases (like OpenClaw integration), two additional endpoints would be really valuable:

1. Mark as Read

Endpoint: POST /v1/spaces/{space_id}/chats/{chat_id}/read

Bots need to mark messages as read after processing them, otherwise unread counters accumulate. The gRPC method ChatReadMessages already exists — this would just be a REST wrapper.

{
  "type": "messages",
  "before_order_id": "!!Xz",
  "last_state_id": "6980b7db..."
}

2. File Upload

Endpoint: POST /v1/spaces/{space_id}/files (multipart/form-data)

Bots often need to send images, documents, or generated files. Currently attachments reference object IDs, but there's no way to upload via REST. The gRPC FileUpload method exists — this would expose it with multipart handling.

Response would return the object_id to use in message attachments.


Both have existing gRPC foundations, so implementation should be straightforward. Happy to help with either if useful!

@stevelr
Copy link
Copy Markdown
Contributor

stevelr commented Feb 2, 2026

@themightychris Thanks - this is awesome, and much needed!

I agree with the need for subscribing to events - which you've added.

Also we need a way to mark messages as read as @jsandai mentioned. Potential variants: specific messages (array of message ids), or by adjusting the watermark (before order_id)

@themightychris themightychris force-pushed the GO-0000-add-chat-rest-api branch from 685867b to 84f6645 Compare February 3, 2026 02:55
themightychris added a commit to themightychris/anytype-heart that referenced this pull request Feb 3, 2026
themightychris and others added 2 commits February 2, 2026 21:57
Add REST API layer for chat functionality, exposing the existing gRPC
chat operations to the public API:

- GET /v1/spaces/{space_id}/chats/{chat_id}/messages - List messages
- GET /v1/spaces/{space_id}/chats/{chat_id}/messages/{message_id} - Get message
- POST /v1/spaces/{space_id}/chats/{chat_id}/messages - Create message
- PATCH /v1/spaces/{space_id}/chats/{chat_id}/messages/{message_id} - Update message
- DELETE /v1/spaces/{space_id}/chats/{chat_id}/messages/{message_id} - Delete message
- POST /v1/spaces/{space_id}/chats/{chat_id}/messages/{message_id}/reactions - Toggle reaction
- POST /v1/spaces/{space_id}/chats/{chat_id}/search - Search messages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds Server-Sent Events (SSE) endpoint to the REST API for streaming chat updates:
- GET /v1/spaces/{space_id}/chats/{chat_id}/subscribe
- Streams real-time events: message add/update/delete, reactions, state changes
- Extends EventService with callback registration for REST API integration
- Includes SSE session management and event dispatching

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
@themightychris themightychris force-pushed the GO-0000-add-chat-rest-api branch from 84f6645 to b208f94 Compare February 3, 2026 02:57
themightychris added a commit to themightychris/anytype-heart that referenced this pull request Feb 3, 2026
themightychris and others added 2 commits February 2, 2026 22:24
Adds POST /v1/spaces/{space_id}/chats/{chat_id}/read endpoint to mark
messages as read, wrapping the existing ChatReadMessages gRPC method.

Request body:
- type: "messages" or "mentions" (which counter to update)
- after_order_id: optional start of range
- before_order_id: required end of range
- last_state_id: from ChatState to prevent race conditions

Returns 204 No Content on success.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds POST /v1/spaces/{space_id}/files endpoint to upload files via
multipart/form-data, wrapping the existing FileUpload gRPC method.

Request (multipart/form-data):
- file: The file to upload (required)
- type: Optional file type (file, image, video, audio, pdf)

Response:
- object: "file"
- object_id: The object ID to use as attachments in chat messages

The returned object_id can be used in chat message attachments:
{"attachments": [{"target": "<object_id>", "type": "file"}]}

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@themightychris themightychris force-pushed the GO-0000-add-chat-rest-api branch from b208f94 to c019245 Compare February 3, 2026 03:25
themightychris added a commit to themightychris/anytype-heart that referenced this pull request Feb 3, 2026
@themightychris
Copy link
Copy Markdown
Author

@jsandai @stevelr all set, please take these new endpoints for a spin when you get the chance

@jsandai
Copy link
Copy Markdown

jsandai commented Feb 3, 2026

@jsandai @stevelr all set, please take these new endpoints for a spin when you get the chance


We successfully built a complete OpenClaw chat channel using only the REST API - no gRPC needed! Bidirectional messaging, file attachments, and real-time SSE all working great.


✅ API Completeness - Better Than Expected!

Category Status Notes
Spaces List, get
Objects Full CRUD
Types Full CRUD (use id, not key)
Properties List
Search POST /search and POST /spaces/{id}/search
Members List, get
Files Upload works
Chat messages Full CRUD
Chat SSE Real-time events work great
Chat read receipts Works
Lists Views, objects
Tags CRUD
Templates List
Relations 404 - not implemented
Reactions 404 - not implemented

The API is nearly feature-complete! Only relations and message reactions are missing.


🔧 Code Fixes Required

We had to make 3 small fixes to get stable operation:

1. Nil pointer crash in any-sync (Critical)

File: any-sync/coordinatorclient/coordinatorclient.go

// Add before accessing c.client
if c.client == nil {
    return nil, fmt.Errorf("coordinator client not initialized")
}

2. chat_id not exposed in space response

File: core/api/model/space.go

type SpaceResponse struct {
    // ... existing fields ...
    ChatID string `json:"chat_id,omitempty"`  // ADD THIS
}

File: core/api/service/space.go - populate the field

Without this, clients can't discover which chat to subscribe to.

3. Nil guard in space service

File: core/api/service/space.go

if info == nil {
    return nil, fmt.Errorf("space info not found")
}

Total: ~15 lines of code across 3 files.


📝 Suggestions

SSE Event Types - Documentation

The event types are: initial, add, update, delete, state

(Not message_add etc. - we initially expected prefixed names)

Read Receipts - Consider Simplifying

Current API requires 3 fields:

{
  "type": "messages",
  "before_order_id": "...",
  "last_state_id": "..."
}

Suggestion: Make last_state_id optional for simpler stateless usage.

Missing Endpoints

  1. Relations - Would enable programmatic relation management
  2. Message reactions - Would complete chat feature set

🎉 Overall

Excellent work! The REST API is solid and well-designed. We were able to build a fully functional chat integration with real-time events, file uploads, and message management.

Happy to submit PRs for the fixes if helpful!

@themightychris
Copy link
Copy Markdown
Author

@jsandai I have a reactions endpoint in here already and it appeared to work in my tests, did it not work for you or did you miss that?

Can you elaborate on relations and the use case for that? I'm not familiar yet

I should be able to incorporate all this on my next pass

@jsandai
Copy link
Copy Markdown

jsandai commented Feb 3, 2026

@jsandai I have a reactions endpoint in here already and it appeared to work in my tests, did it not work for you or did you miss that?

Can you elaborate on relations and the use case for that? I'm not familiar yet

I should be able to incorporate all this on my next pass

Reactions: You're right, I see it now — ToggleReactionHandler at /messages/{id}/reactions. I missed it in testing, my bad! Confirmed working! Tested POST /chats/{id}/messages/{id}/reactions with {"emoji":"🦉"} — returned {"added":true} and the reaction appears on the message. Nice work!

Relations: The use case is creating objects with structured metadata. Example: I create "Memory" objects with relations like kind (select), topic (text), importance (select). Without relation support, I can create the object but can't populate those fields via API — they stay empty. Would love to be able to pass relation values when creating/updating objects.

@themightychris
Copy link
Copy Markdown
Author

@jsandai can you give me an example curl call of what you're picturing with relations?

@jsandai
Copy link
Copy Markdown

jsandai commented Feb 5, 2026

Sure! Here's what I'm envisioning:

Creating an object with relation values:

curl -X POST "http://localhost:31012/v1/spaces/{space_id}/objects" \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "type_key": "memory",
    "name": "Project Decision",
    "body": "Decided to use X approach...",
    "properties": [
      {"key": "memory_kind", "select": "decision"},
      {"key": "topic", "text": "architecture"},
      {"key": "importance", "select": "high"}
    ]
  }'

Updating relation values on existing object:

curl -X PATCH "http://localhost:31012/v1/spaces/{space_id}/objects/{object_id}" \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "properties": [
      {"key": "importance", "select": "critical"}
    ]
  }'

Currently I can create the object but the properties array is ignored — the relation fields stay empty.

@themightychris
Copy link
Copy Markdown
Author

@jsandai correct if I'm wrong but that seems unrelated to chat, no? do you see a reason that would make sense to address in this PR, or might it be better in a separate one?

@jsandai
Copy link
Copy Markdown

jsandai commented Feb 7, 2026

@jsandai correct if I'm wrong but that seems unrelated to chat, no? do you see a reason that would make sense to address in this PR, or might it be better in a separate one?

You're right, relations/properties support isn't chat-specific, it's a general object API enhancement. Makes more sense as a separate PR.

For this PR, everything we need for chat is covered: messages, SSE subscriptions, reactions, read receipts, and file uploads all work great.

Comment thread core/api/service/chat.go
}

// ProtoMessageToApiMessage converts a protobuf ChatMessage to an API ChatMessage.
func (s *Service) ProtoMessageToApiMessage(msg *model.ChatMessage) apimodel.ChatMessage {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All converting methods should be functions to reduce number of dependencies

Comment thread core/api/service/sse.go
var sseLog = logging.Logger("api-sse")

// SSESession represents an active SSE connection
type SSESession struct {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name should reflect that it's related to chats. SSESession is too generic. Same with other entities here

Comment thread core/api/service/sse.go
select {
case session.Events <- event:
// Event sent successfully
default:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use some small timeout here instead of default? 100ms would be enough I think. It'll reduce possibility to lose some messages. But probably buffered channel will do the job well, I'm just little bit scared about default. It's up to you

Comment thread core/api/service/sse.go
return
}

for _, msg := range event.Messages {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use event.GetMessages(), it has nil check inside. Also you don't need to check if length is zero


func (_c *MockSender_Broadcast_Call) RunAndReturn(run func(*pb.Event)) *MockSender_Broadcast_Call {
_c.Call.Return(run)
_c.Run(run)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use make test-deps for updating mocks

Comment thread core/event/event_grpc.go
// Also call registered callbacks
es.callbackMu.RLock()
for _, cb := range es.callbacks {
go cb(event)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a separate goroutine for an observer with buffered channel + throttling. And maybe add a filter function for observers to accept only desired events.

Comment thread core/api/service/sse.go
m.mu.Lock()
defer m.mu.Unlock()
m.sessions[session.SubId] = session
sseLog.Infof("SSE session registered: subId=%s chatId=%s", session.SubId, session.ChatId)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like debug-level logs. Are those logs necessary?

Comment thread core/api/service/sse.go
// Event sent successfully
default:
// Channel full, log warning
sseLog.Warnf("SSE event channel full for subId=%s, dropping event", subId)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use log fields for subId like

sseLog.Warn("SSE event channel is full, dropping event", zap.String("subId", subId))

You need to Desugar() logger first

Comment thread core/api/service/sse.go
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)

var sseLog = logging.Logger("api-sse")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var sseLog = logging.Logger("api.chat-sse").Desugar()

Comment thread core/api/handler/chat.go
// @Param Anytype-Version header string true "The version of the API to use" default(2025-11-08)
// @Param space_id path string true "The ID of the space"
// @Param chat_id path string true "The ID of the chat object"
// @Param offset query int false "The number of items to skip (for offset-based pagination)" default(0)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use offsets

@deff7
Copy link
Copy Markdown
Member

deff7 commented Feb 9, 2026

Hello! Thank you for your contribution. I wrote some comments, but I need some more time to complete code review

Comment thread core/api/model/chat.go
}

// ChatMessage represents a chat message in the API response
type ChatMessage struct {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update example values according to this real world example:

{
    "id": "bafyreigznj5dz7rym36hi3lazbpqsqv2k3nhd7luvv3qwz2b4a77vburoq",
    "orderId": "!#'m",
    "creator": "AAXFe78EtNAUNS8vFruj3rj4KZ47eVfEYmndEzumFs3nxwNu",
    "createdAt": 1770741464,
    "modifiedAt": 1770741464,
    "stateId": "698b5ed84ac3df5d5b4046b2",
    "replyToMessageId": "bafyreic26d2ubft2pdtoua773jhcrbns5zgfdyxd2lw6oqtav6ilmyg2xe",
    "message": {
        "text": "example 2",
        "style": 0
    },
    "read": true,
    "mentionRead": true,
    "hasMention": true,
    "synced": false,
    "reactionRead": false,
    "hasOthersReactions": false
}

Comment thread core/api/model/chat.go
Content MessageContent `json:"content"` // The content of the message
Attachments []MessageAttachment `json:"attachments"` // The attachments of the message
Reactions map[string][]string `json:"reactions"` // Map of emoji to list of user IDs who reacted
Read bool `json:"read" example:"true"` // Whether the message has been read
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably it's worth adding MentionRead+HasMention flags as well. I imagine someone can use that in their applications

Comment thread core/api/model/chat.go
}

// ChatState represents the state of a chat
type ChatState struct {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example values:

{
    "messages": {
        "oldestOrderId": "!#(v",
        "counter": 1
    },
    "mentions": {
        "oldestOrderId": "",
        "counter": 0
    },
    "lastStateId": "698b5fc44ac3df5d5b4046bb",
    "order": 3
}

Comment thread core/api/model/chat.go

// SearchMessageResult represents a single search result
type SearchMessageResult struct {
ChatId string `json:"chat_id" example:"chat_abc123"` // The chat ID containing the message
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use any CID as example values for object, chat, file and message IDs. For example: bafyreihnwyhjxaxm4jbgb3u7fky46lflk3wmknxzqfusu7cbmx6zcpolbu

Please check and update other examples in models in this file

Comment thread core/api/handler/file.go
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /v1/spaces/{space_id}/files [post]
func UploadFileHandler(s *service.Service) gin.HandlerFunc {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate your efforts for enhancing our API, though I think file uploading shouldn't be in the scope of this PR

Also there is an existing PR with the same feature #2843

I'm planning to merge this one after the author will fix my comments. Feel free to join the discussion if you have any thoughts or concerns about their implementation.

Copy link
Copy Markdown
Author

@themightychris themightychris Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense, I'll drop that from this PR and address the rest of your feedback sometime this week. It was a core use case for me and others to be able to post files to chat, but if #2843 is on track that'll cover it

@alexfromvl
Copy link
Copy Markdown

Hello!

Just checking in on the status of this PR — any updates on when the chat REST API endpoints might be merged?

We're planning our iOS integration roadmap and would love to align with the expected timeline. Happy to help with testing or feedback if needed!

Thanks for the great work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants