molroomolroo
API Reference

WebSocket

Real-time emotion updates via persistent WebSocket connections.

WebSocket

The WebSocket endpoint provides a persistent, bidirectional connection for real-time emotion updates. Instead of polling the REST API after each interaction, maintain a WebSocket connection to receive instant state changes and server-pushed proactive actions.

Built on Cloudflare Durable Objects with the WebSocket Hibernation API, idle connections have zero compute cost.


Connect

GET /v1/ws/:sessionId?token=YOUR_API_KEY

Establish a WebSocket connection for a session. Authentication is provided via the token query parameter since the WebSocket protocol does not support custom headers.

Query Parameters

ParameterTypeRequiredDescription
tokenstringYesYour API key

Path Parameters

ParameterTypeRequiredDescription
sessionIdstringYesThe session ID to connect to

Connection Example

JavaScript

const ws = new WebSocket(
  "wss://api.molroo.io/v1/ws/session_xyz?token=YOUR_API_KEY"
);
 
ws.onopen = () => {
  console.log("Connected to molroo emotion stream");
};
 
ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  console.log(`[${message.type}]`, message);
};
 
ws.onerror = (error) => {
  console.error("WebSocket error:", error);
};
 
ws.onclose = (event) => {
  console.log(`Disconnected: ${event.code} ${event.reason}`);
};

Python

import asyncio
import json
import websockets
 
async def connect():
    uri = "wss://api.molroo.io/v1/ws/session_xyz?token=YOUR_API_KEY"
 
    async with websockets.connect(uri) as ws:
        print("Connected to molroo emotion stream")
 
        async for raw in ws:
            message = json.loads(raw)
            print(f"[{message['type']}]", message)
 
asyncio.run(connect())

Client to Server Messages

All messages are JSON objects with a type field.

Simplified Turn

Send a text message for emotion processing:

{
  "type": "turn",
  "message": "The user just told the character they got promoted at work"
}

Full Turn

Send a turn with full event details and optional timing:

{
  "type": "turn",
  "event": {
    "description": "The user shared exciting news about a job promotion",
    "source": "user"
  },
  "elapsed_seconds": 60
}

Appraisal-Based Turn

Provide a pre-computed appraisal vector instead of a text description:

{
  "type": "turn_appraisal",
  "appraisal": {
    "goal_relevance": 0.9,
    "goal_congruence": 0.85,
    "expectedness": 0.3,
    "controllability": 0.5,
    "agency": -0.2,
    "norm_compatibility": 0.7
  },
  "context": "Player character leveled up after defeating the boss",
  "elapsed_seconds": 5
}

Request State

Request a full state snapshot:

{
  "type": "get_state"
}

Set Proactive Preferences

Update proactive behavior preferences for the session:

{
  "type": "set_proactive_prefs",
  "preferences": {
    "enabled": true,
    "triggers": {
      "attachment_seeking": true,
      "need_deficit": true,
      "emotional_overflow": false
    },
    "cooldown_hours": 4
  }
}

Acknowledge Proactive Action

Acknowledge receipt of a proactive action to prevent re-delivery:

{
  "type": "ack_proactive",
  "action_id": "act_abc123"
}

Server to Client Messages

All messages are JSON objects with a type field.

Connected

Sent immediately after the WebSocket connection is established:

{
  "type": "connected",
  "session_id": "session_xyz"
}

Turn Result

Returned after processing a turn or appraisal-based turn:

{
  "type": "turn_result",
  "data": {
    "appraisal": {
      "goal_relevance": 0.85,
      "goal_congruence": 0.9,
      "expectedness": 0.3,
      "controllability": 0.5,
      "agency": 0.1,
      "norm_compatibility": 0.7
    },
    "new_emotion": { "V": 0.72, "A": 0.58, "D": 0.31 },
    "discrete_emotion": "joy",
    "emotion_intensity": 0.68,
    "body_budget": { "energy": 0.85, "stress": 0.15 },
    "prompt_data": { "..." : "..." }
  }
}

State

Full state snapshot, returned in response to get_state:

{
  "type": "state",
  "data": {
    "emotion": { "V": 0.72, "A": 0.58, "D": 0.31 },
    "discrete_emotion": "joy",
    "emotion_intensity": 0.68,
    "body_budget": { "energy": 0.85, "stress": 0.15 },
    "soul_stage": { "stage": "bonding", "progress": 0.45 },
    "need_state": { "belonging": 0.7, "competence": 0.6, "autonomy": 0.8 },
    "interpersonal": { "rapport": 0.55, "trust": 0.72, "familiarity": 0.6 }
  }
}

Proactive

Server-pushed proactive action (requires proactive behavior to be enabled):

{
  "type": "proactive",
  "data": {
    "action_id": "act_abc123",
    "action_type": "attachment_seeking",
    "message": "Hey, I've been thinking about what you said earlier...",
    "priority": "medium"
  }
}

Missed Proactive

Queued actions delivered on reconnect if proactive actions occurred while the client was disconnected:

{
  "type": "missed_proactive",
  "data": [
    {
      "action_id": "act_abc123",
      "action_type": "need_deficit",
      "message": "I'd love to try something creative together",
      "priority": "low",
      "timestamp": "2025-01-15T10:30:00Z"
    }
  ]
}

Full Example

A complete example showing turn processing and proactive action handling over WebSocket:

JavaScript

const ws = new WebSocket(
  "wss://api.molroo.io/v1/ws/session_xyz?token=YOUR_API_KEY"
);
 
ws.onopen = () => {
  // Enable proactive behavior
  ws.send(JSON.stringify({
    type: "set_proactive_prefs",
    preferences: {
      enabled: true,
      triggers: {
        attachment_seeking: true,
        need_deficit: true,
        emotional_overflow: true,
      },
      cooldown_hours: 2,
    },
  }));
};
 
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
 
  switch (msg.type) {
    case "connected":
      console.log(`Connected to session: ${msg.session_id}`);
      break;
 
    case "turn_result":
      console.log(`Emotion: ${msg.data.discrete_emotion} (${msg.data.emotion_intensity})`);
      console.log(`VAD: V=${msg.data.new_emotion.V}, A=${msg.data.new_emotion.A}, D=${msg.data.new_emotion.D}`);
      // Use msg.data.prompt_data to enrich your LLM prompt
      break;
 
    case "state":
      console.log("Full state:", msg.data);
      break;
 
    case "proactive":
      console.log(`Proactive [${msg.data.priority}]: ${msg.data.message}`);
      // Acknowledge to prevent re-delivery
      ws.send(JSON.stringify({
        type: "ack_proactive",
        action_id: msg.data.action_id,
      }));
      break;
 
    case "missed_proactive":
      console.log(`${msg.data.length} missed actions while disconnected`);
      msg.data.forEach((action) => {
        console.log(`  [${action.priority}] ${action.message}`);
        ws.send(JSON.stringify({
          type: "ack_proactive",
          action_id: action.action_id,
        }));
      });
      break;
  }
};
 
// Send a turn
function sendTurn(description) {
  ws.send(JSON.stringify({
    type: "turn",
    message: description,
  }));
}
 
// Send an appraisal-based turn
function sendAppraisal(appraisal, context) {
  ws.send(JSON.stringify({
    type: "turn_appraisal",
    appraisal,
    context,
  }));
}
 
// Request full state
function getState() {
  ws.send(JSON.stringify({ type: "get_state" }));
}

Python

import asyncio
import json
import websockets
 
async def main():
    uri = "wss://api.molroo.io/v1/ws/session_xyz?token=YOUR_API_KEY"
 
    async with websockets.connect(uri) as ws:
        # Enable proactive behavior
        await ws.send(json.dumps({
            "type": "set_proactive_prefs",
            "preferences": {
                "enabled": True,
                "triggers": {
                    "attachment_seeking": True,
                    "need_deficit": True,
                    "emotional_overflow": True,
                },
                "cooldown_hours": 2,
            },
        }))
 
        # Send a turn
        await ws.send(json.dumps({
            "type": "turn",
            "message": "The user told the character about their promotion",
        }))
 
        # Listen for responses
        async for raw in ws:
            msg = json.loads(raw)
 
            if msg["type"] == "connected":
                print(f"Connected to session: {msg['session_id']}")
 
            elif msg["type"] == "turn_result":
                data = msg["data"]
                print(f"Emotion: {data['discrete_emotion']} ({data['emotion_intensity']})")
 
            elif msg["type"] == "proactive":
                data = msg["data"]
                print(f"Proactive [{data['priority']}]: {data['message']}")
                await ws.send(json.dumps({
                    "type": "ack_proactive",
                    "action_id": data["action_id"],
                }))
 
            elif msg["type"] == "missed_proactive":
                print(f"{len(msg['data'])} missed actions")
                for action in msg["data"]:
                    await ws.send(json.dumps({
                        "type": "ack_proactive",
                        "action_id": action["action_id"],
                    }))
 
asyncio.run(main())

Connection Details

PropertyValue
Protocolwss:// (TLS required)
AuthenticationQuery parameter token (custom headers not supported)
Idle costZero (WebSocket Hibernation API on Durable Objects)
ReconnectionMissed proactive actions are queued and delivered on reconnect
Message formatJSON

Errors

WebSocket errors are delivered as JSON messages:

{
  "type": "error",
  "code": "UNAUTHORIZED",
  "message": "Invalid or missing API key"
}
CodeDescription
UNAUTHORIZEDInvalid or missing token
NOT_FOUNDSession not found
VALIDATION_ERRORInvalid message format
RATE_LIMITToo many messages