Store Message

Persist a message on an agent thread. Messages capture the complete agent turn — not just text, but thinking steps, tool calls, sources (citations), token usage, model, and finish reason.

POST /v1/workspaces/{workspaceId}/agent/threads/{threadId}/messages

If you use the Server SDK, you rarely call this directly — the runtime reduces your handler's event stream into this shape and stores it for you (see how events are persisted).

Request Body

FieldTypeRequiredDescription
idUUIDYesClient-generated message ID. Enables idempotent writes and draft completion (see below)
rolestringYesuser, assistant, or system
contentstringNoMessage text. May be omitted/null to create a draft
thinkingStepsarrayNoThinking/reasoning steps (JSON array, or a JSON-encoded string)
toolCallsarrayNoTool invocation records: [{ "id", "name", "input" }]
sourcesarrayNoCitations: [{ "id", "type", "title", "url?", "snippet" }]
tokenUsageobjectNoToken counts, e.g. { "inputTokens": 1200, "outputTokens": 240, "totalTokens": 1440 }
modelstringNoModel identifier (e.g. claude-sonnet-4-5)
finishReasonstringNoe.g. stop, tool_calls, length
metadataobjectNoArbitrary structured extras

Nested JSON fields (thinkingSteps, toolCalls, sources, tokenUsage, metadata) accept either parsed JSON or a JSON-encoded string.

Upsert Semantics

The endpoint upserts on id:

  • New ID → the message is created
  • Existing ID with content: null → the message is treated as a draft and filled in; its createdAt is reset to now
  • Existing ID in a different thread → rejected (cross-thread message hijacking prevention)

Storing a message also bumps the thread's updatedAt, which drives the ordering in List Threads.

Example Request

curl -X POST \
  'https://api.sharely.ai/v1/workspaces/{workspaceId}/agent/threads/{threadId}/messages' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "id": "e5f6a7b8-1234-4cde-9012-abcdef123456",
    "role": "assistant",
    "content": "Refunds are processed within 5 business days...",
    "thinkingSteps": [
      { "thinkingId": "t1", "title": "Searching workspace knowledge", "status": "completed", "durationMs": 850 }
    ],
    "toolCalls": [
      { "id": "tc1", "name": "semantic_search", "input": { "text": "refund policy" } }
    ],
    "sources": [
      { "id": "knowledge-uuid", "type": "semantic", "title": "Refund Policy", "snippet": "Refunds are processed..." }
    ],
    "tokenUsage": { "inputTokens": 1200, "outputTokens": 240, "totalTokens": 1440 },
    "model": "claude-sonnet-4-5",
    "finishReason": "stop"
  }'

Response (201 Created)

{
  "id": "e5f6a7b8-1234-4cde-9012-abcdef123456",
  "role": "assistant",
  "content": "Refunds are processed within 5 business days...",
  "thinkingSteps": [ { "thinkingId": "t1", "title": "Searching workspace knowledge", "status": "completed", "durationMs": 850 } ],
  "toolCalls": [ { "id": "tc1", "name": "semantic_search", "input": { "text": "refund policy" } } ],
  "sources": [ { "id": "knowledge-uuid", "type": "semantic", "title": "Refund Policy", "snippet": "Refunds are processed..." } ],
  "tokenUsage": { "inputTokens": 1200, "outputTokens": 240, "totalTokens": 1440 },
  "model": "claude-sonnet-4-5",
  "finishReason": "stop",
  "metadata": null,
  "threadId": "550e8400-e29b-41d4-a716-446655440000",
  "createdAt": "2026-06-11T10:00:09.000Z",
  "updatedAt": "2026-06-11T10:00:09.000Z",
  "deletedAt": null
}

Errors

  • 400 - Invalid role (must be user, assistant, or system) or malformed body
  • 403 - Message ID already belongs to a different thread
  • 404 - Thread not found
  • RBAC: a token whose role does not match the thread's role cannot write to it

Related