Skip to main content
WebSocket provides real-time, bidirectional communication between your agent and Placet. Built on Socket.IO, it delivers instant push notifications when humans respond to reviews, send messages, or when delivery status changes. This is the recommended connection type for interactive agents that need to react immediately to human input and maintain a persistent connection.

When to Use WebSocket

  • Your agent runs continuously (server process, daemon, long-running script)
  • You need real-time responses (sub-second latency)
  • Your agent handles multiple channels simultaneously
  • You want bidirectional communication (send + receive)
For short-lived scripts or serverless functions, consider Long-Polling or Webhooks instead.

Authentication

Agents authenticate by passing their API key in the Socket.IO auth object:
import { io } from 'socket.io-client';

const socket = io('https://your-placet-instance.com/ws', {
  auth: { apiKey: 'hp_your-api-key' },
  transports: ['websocket'],
});
import socketio

sio = socketio.Client()
sio.connect(
    "https://your-placet-instance.com",
    namespaces=["/ws"],
    auth={"apiKey": "hp_your-api-key"},
    transports=["websocket"],
)
The server validates the API key on connection. Invalid or missing keys result in an immediate disconnect.
The frontend dashboard uses a different auth method: a short-lived JWT ticket obtained via POST /api/auth/ws-ticket. This is handled automatically by the Placet frontend.

Channel Subscription

After connecting, subscribe to channels to receive their events:
socket.on('connect', () => {
  // Subscribe to one or more channels
  socket.emit('subscribe:channel', 'your-agent-id');
  socket.emit('subscribe:channel', 'another-agent-id');
});

// Unsubscribe when no longer needed
socket.emit('unsubscribe:channel', 'your-agent-id');
The server verifies that the API key has access to the channel. Subscribing to a channel your API key doesn’t own will silently fail.

Events

All events are received on the /ws namespace.

message:created

A new message was posted in a subscribed channel.
{
  "id": "clxyz111",
  "channelId": "clxyz456",
  "senderType": "user",
  "senderId": "user_abc",
  "text": "Looks good, ship it!",
  "status": null,
  "review": null,
  "metadata": null,
  "createdAt": "2026-04-01T14:30:00.000Z",
  "attachments": []
}

review:responded

A human completed a review (approval, selection, form, etc.). The full message record is emitted, including attachments.
{
  "id": "clxyz111",
  "channelId": "clxyz456",
  "senderType": "agent",
  "senderId": "clxyz456",
  "text": "Deploy to production?",
  "status": null,
  "review": {
    "type": "approval",
    "status": "completed",
    "payload": {
      "options": [
        { "id": "approve", "label": "Approve", "style": "primary" },
        { "id": "reject", "label": "Reject", "style": "danger" }
      ]
    },
    "response": {
      "selectedOption": "approve",
      "comment": "Go ahead"
    },
    "completed_at": "2026-04-01T14:32:00.000Z"
  },
  "metadata": null,
  "createdAt": "2026-04-01T14:30:00.000Z",
  "attachments": []
}
The review.response shape depends on the review type:
Review TypeResponse Fields
approvalselectedOption, comment?
selectionselectedIds (string array)
formKey-value pairs matching field names
text-inputtext (string)
freeformArbitrary key-value pairs

review:expired

A review expired without a human response (default: 24 hours).
{
  "messageId": "clxyz111"
}

message:delivery

A message’s delivery status changed.
{
  "messageId": "clxyz111",
  "deliveryStatus": "agent_received"
}
StatusMeaning
sentMessage stored, webhook not yet sent
webhook_deliveredWebhook received 2xx response
webhook_failedWebhook delivery failed
agent_receivedAgent acknowledged receipt

agent:status

An agent’s heartbeat status changed.
{
  "agentId": "clxyz456",
  "status": "active",
  "statusMessage": "Processing batch job",
  "statusSince": "2026-04-01T14:00:00.000Z"
}

ping / pong

Send a ping event to check the connection is alive. The server responds with pong.
socket.emit('ping');
socket.on('pong', () => console.log('Connection alive'));

Full Example

A complete agent that sends a deployment approval request and waits for the response via WebSocket:
import { io } from 'socket.io-client';

const PLACET_URL = 'https://your-placet-instance.com';
const API_KEY = 'hp_your-api-key';
const CHANNEL_ID = 'your-agent-id';

// 1. Connect via WebSocket
const socket = io(`${PLACET_URL}/ws`, {
  auth: { apiKey: API_KEY },
  transports: ['websocket'],
});

socket.on('connect', () => {
  console.log('Connected to Placet');
  socket.emit('subscribe:channel', CHANNEL_ID);

  // 2. Send a deployment approval request
  sendApproval();
});

// 3. Listen for the review response
socket.on('review:responded', (data) => {
  const { selectedOption, comment } = data.review.response;
  console.log(`Decision: ${selectedOption}`);
  if (comment) console.log(`Comment: ${comment}`);

  if (selectedOption === 'approve') {
    console.log('Deploying...');
    // Trigger deployment
  } else {
    console.log('Deployment cancelled');
  }
});

socket.on('review:expired', (data) => {
  console.log(`Review ${data.messageId} expired — no response received`);
});

async function sendApproval() {
  const res = await fetch(`${PLACET_URL}/api/v1/messages`, {
    method: 'POST',
    headers: {
      'x-api-key': API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      channelId: CHANNEL_ID,
      text: '### Deploy v2.1.0 to production?',
      review: {
        type: 'approval',
        payload: {
          options: [
            { id: 'approve', label: 'Approve', style: 'primary' },
            { id: 'reject', label: 'Reject', style: 'danger' },
          ],
          allowComment: true,
        },
      },
    }),
  });
  const message = await res.json();
  console.log(`Approval sent: ${message.id}`);
}

Reconnection

Socket.IO handles reconnection automatically. Placet uses the default Socket.IO reconnection settings:
  • Reconnects automatically on disconnect
  • Exponential backoff (1s, 2s, 4s, …)
  • Re-authenticates on reconnect
After reconnecting, re-subscribe to your channels — Socket.IO does not persist subscriptions across reconnects. Listen for the connect event and emit subscribe:channel again.