Documentation
Project Context API

Project Context API

Real-time, project-scoped WebSocket API for live sensor data, variables, events, and bidirectional control.

The Project Context API gives any application — display previews, dashboards, integrations — a standard way to subscribe to a project's live data and send commands back.

Quick Start

1. Create an API Key

Generate a key in the Palpable app under Project Settings > API Keys, or via the API Keys API.

Key format: pk_{8-char}_{32-char}
Example:    pk_your_api_key

Keys can be read-only (subscribe to data) or read-write (also set variables and trigger behaviors).

2. Discover the WebSocket URL

const res = await fetch(
  `https://palpable.technology/api/projects/${projectId}/connect`,
  { headers: { Authorization: `Bearer ${apiKey}` } }
)
const { wsUrl } = await res.json()

Response:

{
  "wsUrl": "wss://agent.palpable.technology/session/{userId}/ctx/{projectId}?token=pk_your_api_key",
  "projectId": "550e8400-...",
  "permissions": ["read"]
}

3. Connect and Subscribe

const ws = new WebSocket(wsUrl)
 
ws.onopen = () => {
  ws.send(JSON.stringify({
    type: 'subscribe',
    channels: ['sensors', 'variables', 'events', 'status']
  }))
}
 
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data)
  console.log(msg.type, msg)
}

Channels

ChannelDescription
sensorsLive sensor readings from connected hardware
variablesBehavior variable state changes
eventsBehavior execution events (triggered, error)
statusDevice connection status and module list

Subscribe to any combination of channels. You'll only receive messages for channels you've subscribed to.

WebSocket Protocol

Client to Server

subscribe

Subscribe to one or more channels. Triggers a snapshot of current state.

{ "type": "subscribe", "channels": ["sensors", "variables"] }

unsubscribe

Stop receiving messages for specific channels.

{ "type": "unsubscribe", "channels": ["events"] }

set_variable

Set a behavior variable on the device. Requires write permission.

{ "type": "set_variable", "name": "score", "value": 42 }

trigger_behavior

Manually trigger a behavior. Requires write permission.

{ "type": "trigger_behavior", "behaviorId": "550e8400-..." }

ping

Keep the connection alive.

{ "type": "ping" }

Server to Client

subscribed

Confirms your current channel subscriptions.

{ "type": "subscribed", "channels": ["sensors", "variables"] }

snapshot

Full state snapshot, sent immediately after subscribing.

{
  "type": "snapshot",
  "sensors": {
    "modulino-knob": { "encoder": 42 },
    "bme280": { "temperature": 22.5, "humidity": 45 }
  },
  "variables": { "score": 10, "mode": "active" },
  "status": {
    "connected": true,
    "modules": [
      { "moduleId": "modulino-knob", "name": "Modulino Knob", "address": 60 }
    ]
  }
}

sensor_data

Live sensor reading update.

{
  "type": "sensor_data",
  "channel": "sensors",
  "moduleId": "modulino-knob",
  "value": { "encoder": 42 },
  "timestamp": 1709142000000
}

variable_state

Variable values changed.

{
  "type": "variable_state",
  "channel": "variables",
  "variables": { "score": 10 }
}

behavior_event

A behavior was triggered or errored.

{
  "type": "behavior_event",
  "channel": "events",
  "event": "behavior_executed",
  "behaviorId": "550e8400-...",
  "behaviorName": "Turn on lights",
  "timestamp": 1709142000000
}

device_status

Device connected/disconnected or modules changed.

{
  "type": "device_status",
  "channel": "status",
  "connected": true,
  "modules": [
    { "moduleId": "bme280", "name": "BME280", "address": 118 }
  ]
}

error

Something went wrong.

{ "type": "error", "message": "Permission denied: write access required", "code": "FORBIDDEN" }

Error codes: FORBIDDEN, INVALID_MESSAGE, NOT_FOUND, INTERNAL.

Authentication

API Key Format

pk_{prefix}_{secret}
  • prefix (8 chars): displayed in the dashboard for identification
  • secret (32 chars): the actual secret, never stored — only its SHA-256 hash is in the database

Permissions

PermissionCapabilities
readSubscribe to all channels, receive snapshots and live data
writeEverything in read, plus set_variable and trigger_behavior

Key Expiry

Keys can optionally have an expires_at timestamp. Expired keys are rejected on connect and on the discovery endpoint.

Examples

Live Dashboard

async function connectToProject(projectId, apiKey) {
  const res = await fetch(
    `https://palpable.technology/api/projects/${projectId}/connect`,
    { headers: { Authorization: `Bearer ${apiKey}` } }
  )
  const { wsUrl } = await res.json()
 
  const ws = new WebSocket(wsUrl)
 
  ws.onopen = () => {
    ws.send(JSON.stringify({
      type: 'subscribe',
      channels: ['sensors', 'variables']
    }))
  }
 
  ws.onmessage = (event) => {
    const msg = JSON.parse(event.data)
 
    switch (msg.type) {
      case 'snapshot':
        console.log('Initial state:', msg.sensors, msg.variables)
        break
      case 'sensor_data':
        console.log(`${msg.moduleId}:`, msg.value)
        break
      case 'variable_state':
        console.log('Variables changed:', msg.variables)
        break
    }
  }
 
  return ws
}

Setting Variables from an External App

// Requires a key with 'write' permission
ws.send(JSON.stringify({
  type: 'set_variable',
  name: 'target_temperature',
  value: 25
}))

Advanced Examples

Dashboard with Multiple Channels

Subscribe to sensors, variables, and events simultaneously to build a real-time dashboard:

const ws = new WebSocket(wsUrl)
 
ws.onopen = () => {
  ws.send(JSON.stringify({
    type: 'subscribe',
    channels: ['sensors', 'variables', 'events', 'status']
  }))
}
 
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data)
 
  switch (msg.type) {
    case 'sensor_data':
      updateSensorChart(msg.moduleId, msg.capability, msg.value)
      break
    case 'variable_update':
      updateVariableDisplay(msg.name, msg.value)
      break
    case 'behavior_event':
      addToEventLog(msg.behaviorName, msg.action, msg.timestamp)
      break
    case 'device_status':
      updateConnectionBadge(msg.status) // 'online' | 'offline'
      break
  }
}

Bidirectional Control Panel

Build a control panel that reads sensor data AND sends commands back:

// Subscribe to sensors
ws.send(JSON.stringify({ type: 'subscribe', channels: ['sensors', 'variables'] }))
 
// Toggle a variable (e.g., light switch)
function toggleLight() {
  ws.send(JSON.stringify({
    type: 'set_variable',
    name: 'light_on',
    value: !currentLightState
  }))
}
 
// Trigger a behavior manually
function runBehavior(behaviorId) {
  ws.send(JSON.stringify({
    type: 'trigger_behavior',
    behaviorId
  }))
}

Display Preview Integration

Connect a custom display preview to live project data:

<!-- Embed in an iframe or standalone page -->
<iframe
  src="https://your-container.palpable.technology/display/i2c:0x3c/preview?projectId=YOUR_PROJECT_ID&token=pk_your_key"
  width="128"
  height="64"
  style="image-rendering: pixelated; transform: scale(4);"
/>

The preview URL automatically connects to the Project Context API and injects live sensor values into the display component's sensors and variables props.