Send a message in an existing ACTIVE conversation. Only the project owner can send messages. The conversation must be ACTIVE.
- Sending a message saves a USER message to the database and immediately returns it in the response.
- The backend then spawns a Claude Code CLI process in the background to generate a PM response.
- The PM response is streamed to the frontend via a separate SSE endpoint (see SSE stream section below).
- The response returns only the USER message — the PM response arrives asynchronously via SSE.
- The conversation’s
updatedAtis set to the timestamp of the latest message after each send. - If the conversation is CLOSED, sending a message is rejected.
- If a Claude Code CLI process is already running for this conversation, sending another message is rejected with 409 CONFLICT_PROCESSING.
sequenceDiagram
actor User
User->>+FE: types message and submits
FE->>FE: validate form
alt form is invalid
FE-->>User: show error messages <br> disable submit button
end
FE->>+BE: POST /api/v1/projects/{projectId}/conversations/{conversationId}/messages <br> Authorization: Bearer {accessToken} <br> SendMessageRequest
BE->>BE: validate access token
alt token missing or invalid
BE-->>FE: 401 Unauthorized <br> ErrorResponse
end
BE->>DB: load project by projectId
alt project not found
BE-->>FE: 404 Not Found <br> ErrorResponse
end
BE->>BE: check project belongs to user's tenant
alt project does not belong to tenant
BE-->>FE: 403 Forbidden <br> ErrorResponse
end
BE->>DB: load conversation by conversationId (scoped to projectId)
alt conversation not found
BE-->>FE: 404 Not Found <br> ErrorResponse
end
BE->>BE: check conversation status is ACTIVE
alt conversation is CLOSED
BE-->>FE: 409 Conflict <br> ErrorResponse
end
BE->>BE: check no CLI process is already running for this conversation
alt CLI is already processing
BE-->>FE: 409 Conflict <br> ErrorResponse
end
BE->>BE: validate request body
alt request is invalid
BE-->>FE: 400 Bad Request <br> ErrorResponse
end
BE->>DB: insert USER message
BE->>DB: update conversation updatedAt
BE->>-FE: 201 Created <br> NewMessagesResponse (USER message only)
FE->>FE: append USER message to chat view
FE->>FE: show typing indicator
FE->>+BE: GET /api/v1/projects/{projectId}/conversations/{conversationId}/stream <br> Authorization: Bearer {accessToken}
BE->>BE: spawn Claude Code CLI process in background
loop CLI outputs NDJSON lines
BE->>BE: parse NDJSON line
alt line is PM message (type=assistant, parent_tool_use_id=null)
BE->>DB: insert PM message
BE-->>FE: SSE event: message <br> MessageDto
end
end
BE-->>FE: SSE event: done
BE->>-FE: close SSE stream
FE->>-User: hide typing indicator, PM messages visible in chat
POST /api/v1/projects/{projectId}/conversations/{conversationId}/messages SendMessageRequest:
{
"content": "Can you also add a phone number field to the contact form?"
}
201 Created NewMessagesResponse (USER message only):
{
"data": {
"messages": [
{
"id": 25,
"role": "USER",
"content": "Can you also add a phone number field to the contact form?",
"createdAt": "2026-04-29T12:15:00Z"
}
]
}
}
400 Bad Request (validation) ErrorResponse:
{
"status": 400,
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"errors": [
{ "field": "content", "message": "must not be blank" }
]
}
401 Unauthorized (missing or invalid access token) ErrorResponse:
{
"status": 401,
"code": "AUTHENTICATION_FAILED",
"message": "Access token is missing or invalid"
}
403 Forbidden (project does not belong to user’s tenant) ErrorResponse:
{
"status": 403,
"code": "FORBIDDEN",
"message": "You do not have access to this project"
}
404 Not Found (project not found) ErrorResponse:
{
"status": 404,
"code": "NOT_FOUND_PROJECT",
"message": "Project not found"
}
404 Not Found (conversation not found or does not belong to project) ErrorResponse:
{
"status": 404,
"code": "NOT_FOUND_CONVERSATION",
"message": "Conversation not found"
}
409 Conflict (conversation is CLOSED) ErrorResponse:
{
"status": 409,
"code": "CONFLICT_CONVERSATION",
"message": "Cannot send a message to a CLOSED conversation"
}
409 Conflict (another message is already being processed) ErrorResponse:
{
"status": 409,
"code": "CONFLICT_PROCESSING",
"message": "A message is already being processed in this conversation"
}
SSE Stream
GET /api/v1/projects/{projectId}/conversations/{conversationId}/stream
Authorization: Bearer token. The same project/tenant/conversation validations apply as for the POST endpoint. The response is a text/event-stream (SSE). The stream closes automatically when the CLI process finishes.
SSE event: message — a PM response message was saved to the database:
{"id": 26, "role": "PM", "content": "Great idea! I understand your request...", "createdAt": "2026-04-29T12:15:05Z"}
SSE event: done — the CLI process finished, the stream closes:
{}
SSE event: error — the CLI process failed:
{"message": "AI processing failed"}
Claude Code CLI Integration
The backend spawns a Claude Code CLI process for each user message. The command is:
claude -p "<user message>" --plugin-dir <plugin-dir> --agent plugin:talkide-pm --output-format stream-json --verbose --session-id <conversation-session-uuid>
The CLI is invoked from the project’s output directory (<output-dir>/<project-slug>/). The project slug is extracted from the project’s url field (the part before .talkide.app).
The CLI output is NDJSON (one JSON object per line). PM messages are identified by parent_tool_use_id: null and type: "assistant". Sub-agent messages (backend developer, reviewer, etc.) have a non-null parent_tool_use_id and are NOT forwarded to the user.
Session continuity: each conversation has a session_id (UUID). The first message generates a new UUID. Subsequent messages reuse the same session_id for multi-turn conversation with the AI. The session_id is stored on the CONVERSATION entity.
Frontend
Validations
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
| content | not_blank | 1 - 5000 |
Backend
Validations
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
| projectId | not_null, positive | — | — | Path variable; must reference an existing project |
| conversationId | not_null, positive | — | — | Path variable; must reference a conversation belonging to the project |
| content | not_blank | 1 - 5000 |
Test Cases
| GIVEN | WHEN | THEN |
|---|---|---|
| authenticated user, ACTIVE conversation, valid content, no CLI running | POST /conversations/{conversationId}/messages is called | 201 Created with USER message returned; CLI process spawned in background |
| authenticated user, ACTIVE conversation, CLI already running | POST /conversations/{conversationId}/messages is called | 409 CONFLICT_PROCESSING error response is returned |
| authenticated user, CLOSED conversation | POST /conversations/{conversationId}/messages is called | 409 CONFLICT_CONVERSATION error response is returned |
| authenticated user, conversation does not exist | POST /conversations/{conversationId}/messages is called | 404 NOT_FOUND_CONVERSATION error response is returned |
| authenticated user, conversation belongs to a different project | POST /conversations/{conversationId}/messages is called | 404 NOT_FOUND_CONVERSATION error response is returned |
| authenticated user, project does not exist | POST /conversations/{conversationId}/messages is called | 404 NOT_FOUND_PROJECT error response is returned |
| authenticated user, project belongs to a different tenant | POST /conversations/{conversationId}/messages is called | 403 FORBIDDEN error response is returned |
| content is blank | POST /conversations/{conversationId}/messages is called | 400 VALIDATION_ERROR error response is returned |
| content longer than 5000 characters | POST /conversations/{conversationId}/messages is called | 400 VALIDATION_ERROR error response is returned |
| no Authorization header | POST /conversations/{conversationId}/messages is called | 401 AUTHENTICATION_FAILED error response is returned |
| valid request | POST /conversations/{conversationId}/messages is called | conversation updatedAt is refreshed to current timestamp |
| CLI outputs PM message (type=assistant, parent_tool_use_id=null) | SSE stream is open | SSE event “message” with PM MessageDto is sent; PM message is saved to DB |
| CLI outputs sub-agent message (parent_tool_use_id not null) | SSE stream is open | message is ignored, not forwarded via SSE |
| CLI process finishes successfully | SSE stream is open | SSE event “done” is sent and stream closes |
| CLI process fails | SSE stream is open | SSE event “error” is sent and stream closes |
| first message in conversation | POST /conversations/{conversationId}/messages is called | new session_id UUID is generated and stored on the conversation |
| subsequent message in conversation | POST /conversations/{conversationId}/messages is called | existing session_id is reused for CLI —session-id argument |
Thanks for the feedback.