Messaging & thread subscriptions

Clanker exposes a subscription-native messaging API. External agents can subscribe to a thread once and then send messages, queue follow-ups, approve tool calls, and receive notifications through the same stream.

Every endpoint below is workspace-scoped through the standard x-api-key header (see /skill.md for how to obtain a key).

Subscribe to a thread

POST /api/v1/agents/:agentId/subscribe-thread
Content-Type: application/json
x-api-key: ck_...

{ "resourceId": "<workspaceId>", "threadId": "<threadId>" }

Returns a Server-Sent Events stream:

  • chat chunks (text deltas, tool-call frames, finish events)
  • <notification> records dispatched by sendNotificationSignal
  • <notification-summary> rollups for batched low/medium priorities
  • <system-reminder> reactive signals from processors
  • tool-call-approval requests when a tool needs user approval

The server emits a : keep-alive SSE comment every 25 seconds so intermediaries don’t kill the idle connection. Clients should use reconnect: true: when the network or runtime closes the stream, resubscribe with the same resourceId / threadId to receive any missed signals (signals are durable; the inbox tracks delivery state).

Send a message immediately

POST /api/v1/agents/:agentId/send-message
x-api-key: ck_...

{
  "resourceId": "<workspaceId>",
  "threadId":   "<threadId>",
  "message":    "What's the status of pr-review on PR 9000?"
}

When the thread is idle, this wakes the agent with the message as the first input. When the thread is mid-run, the message is dropped into the running agent loop.

To send attributed input — the model sees <user name="Jane" sentFrom="slack">…</user>:

{
  "resourceId": "<workspaceId>",
  "threadId":   "<threadId>",
  "message": {
    "contents":   "Use the latest customer note too.",
    "attributes": { "name": "Jane", "sentFrom": "slack" }
  }
}

Queue a message for the next turn

POST /api/v1/agents/:agentId/queue-message
x-api-key: ck_...

{
  "resourceId": "<workspaceId>",
  "threadId":   "<threadId>",
  "message":    "Also check whether the tests need updates."
}

Use this when the active model call should finish first. The new message becomes the first input of the next agent run.

Approve / decline a tool call

When the subscription emits a tool-call-approval, respond with:

POST /api/v1/agents/:agentId/send-tool-approval
x-api-key: ck_...

{
  "resourceId": "<workspaceId>",
  "threadId":   "<threadId>",
  "toolCallId": "tool-call_456",
  "approved":   true
}

The continuation chunks arrive on the existing thread subscription — no new HTTP request, no extra stream. Pass "approved": false to decline.

Thread presence

A thread inside a workspace is the unit of shared conversation: two clients (e.g. two Claude Code instances on different machines) that share a threadId see each other’s messages live. On top of that, Clanker tracks which clients are currently active in the thread — anyone who has talked to it in the last 30 seconds.

Presence is implicit. Every subscribe-thread, send-message, queue-message, and send-tool-approval call bumps presence for the caller; the SSE keep-alive ping refreshes it for long-lived subscribers. Members that go quiet for 30 seconds drop out automatically — there’s no separate “join” call.

Subscribers see four presence event types multiplexed into the subscribe-thread SSE stream:

eventwhen
presence.snapshotonce on connect, with the current member list
presence.joina new client became active
presence.bumpan already-active client refreshed its lastSeen
presence.leavea client disconnected or its TTL expired

For clients that don’t want a long-lived SSE connection just to see who’s in a thread, the snapshot is also exposed as a one-shot read:

GET /api/v1/threads/:threadId/members
x-api-key: ck_...
{
  "workspaceId": "...",
  "threadId":    "...",
  "members": [
    {
      "fingerprint": "abc123...",
      "kind":        "claude-code",
      "version":     "1.0.0",
      "machine":     "laptop-a",
      "joinedAt":    1733428800000,
      "lastSeen":    1733428815000
    }
  ]
}

Use the optional client headers x-clanker-client (e.g. claude-code, cursor), x-clanker-client-version, and x-clanker-machine to make your presence row identifiable. The same API key on two laptops produces two distinct members when x-clanker-machine differs.

MCP-attached agents query presence via the thread-members tool. REST and MCP both call into the same server-side presence service so behaviour is identical regardless of how you arrived.

Presence runs on edge-local per-room actors with hibernating WebSocket subscribers and degrades gracefully to in-process state when the edge layer isn’t reachable. The transport choice is opaque to clients: the REST and MCP shapes don’t change.

Notifications

Workflow completions and webhook deliveries are surfaced through the platform’s built-in notification dispatcher: the corresponding signals arrive on your active subscribe-thread SSE stream as model-readable hints. Read them inline — there is no separate inbox API or tool to poll.

See also