Skip to content

WebSocket

Hantera exposes a unified WebSocket endpoint for real-time communication. It supports two capabilities:

  • Event Streaming — push notifications when things happen in your system (jobs, actor state changes)
  • Live Queries (experimental) — maintain a reactive, server-side query result that automatically updates as data changes

WARNING

Preview API: The WebSocket API is currently in preview and subject to change before final release.

Endpoint

wss://{hostname}/ws

Replace {hostname} with your tenant hostname (e.g. tenant.core.ams.hantera.cloud).

Connection Lifecycle

  1. Open WebSocket connection

    typescript
    const ws = new WebSocket('wss://{hostname}/ws')
  2. Authenticate

    The first message must be an auth message containing your access token:

    json
    {
      "type": "auth",
      "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
    }

    Send the token without the Bearer prefix. Authentication must complete within 10 seconds or the server closes the connection.

  3. Receive confirmation

    On success:

    json
    { "type": "authenticated" }
  4. Subscribe to events or create live queries

    Once authenticated you can send subscribeEvents and createLiveQuery messages (see below).

  5. Respond to keep-alive pings

    The server sends ping every 30 seconds. Respond with pong within 30 seconds:

    json
    { "type": "pong" }

Event Streaming

Subscribe to server-side events using the subscribeEvents message.

Subscribe

json
{
  "type": "subscribeEvents",
  "requestId": "req-1",
  "subscriptions": [
    {
      "id": "my-jobs",
      "path": "jobs",
      "events": ["jobScheduled", "jobStarted", "jobCompleted", "jobFailed"]
    }
  ]
}

The server confirms with subscribedEvents:

json
{
  "type": "subscribedEvents",
  "requestId": "req-1",
  "subscriptions": [
    { "id": "my-jobs", "path": "jobs", "events": ["jobScheduled", "jobStarted", "jobCompleted", "jobFailed"] }
  ]
}

Receiving Events

Events arrive as event messages:

json
{
  "type": "event",
  "subscriptionIds": ["my-jobs"],
  "eventType": "jobCompleted",
  "path": "jobs",
  "data": {
    "jobId": "550e8400-e29b-41d4-a716-446655440000",
    "jobDefinitionId": "sync-inventory",
    "finishedAt": "2025-12-07T21:45:05.000Z",
    "elapsedMs": 4000.5
  },
  "timestamp": "2025-12-07T21:45:05.000Z"
}

Unsubscribe

json
{
  "type": "unsubscribeEvents",
  "ids": ["my-jobs"]
}

Supported Paths and Events

Jobs

PathDescription
jobsAll job lifecycle events
jobs/{jobId}Events for a specific job
EventDescription
jobScheduledJob created in pending state
jobStartedJob execution began
jobCompletedJob finished successfully
jobFailedJob execution failed

Job Statistics

PathDescription
job-definitionsStatistics for all job definitions
job-definitions/{jobDefinitionId}Statistics for a specific job type
EventDescription
jobStatisticsLive bucket update with aggregated counters

Actors

PathDescription
actorsAll actor checkpoint events
actors/{actorType}Checkpoints for a specific actor type
actors/{actorType}/{actorId}Checkpoints for a specific actor instance

Actor types: orders, payments, skus, and custom actors.

EventDescription
actorCheckpointCheckpoint created in actor

INFO

The checkpoint event does not include mutation details. Query the actor's state via the Graph API to see what changed.

Backpressure

Event streaming uses best-effort delivery. When your client consumes events slower than they are produced, the server queues events and drops old ones when the queue fills. A warning message with code QUEUE_OVERFLOW is sent to notify you:

json
{
  "type": "warning",
  "code": "QUEUE_OVERFLOW",
  "message": "5 events dropped for subscription 'my-jobs' due to slow consumption",
  "subscriptionId": "my-jobs"
}

When you receive an overflow warning, re-sync your state from the Graph API to recover any missed changes.

WARNING

Dropped events are permanently lost. For workflows requiring guaranteed delivery, use Rules with webhooks instead.

Live Queries (Experimental)

WARNING

Experimental: Live Queries are an experimental feature. Message shapes and behaviour may change.

A live query runs a Graph API query server-side and sends you the initial results plus incremental updates as data changes.

Create a Live Query

Send a createLiveQuery message with your graph query nested in a query field:

json
{
  "type": "createLiveQuery",
  "id": "lq-orders",
  "query": {
    "edge": "orders",
    "filter": "status == 'processing'",
    "orderBy": "createdAt desc"
  }
}

The server responds with liveQueryCreated:

json
{
  "type": "liveQueryCreated",
  "id": "lq-orders",
  "totalCount": 42,
  "capped": false
}

capped is true when the result set exceeds the maximum record limit (default 1 000). The query will only track the capped set.

Receiving Initial Data

After creation the server streams the initial result set as one or more liveQueryData messages:

json
{
  "type": "liveQueryData",
  "id": "lq-orders",
  "data": [ { "id": "...", "status": "processing", ... }, ... ],
  "hasMore": true,
  "capped": false
}
json
{
  "type": "liveQueryData",
  "id": "lq-orders",
  "data": [ ... ],
  "hasMore": false,
  "capped": false
}

When hasMore is false, the initial load is complete.

Incremental Updates

As underlying data changes, the server sends targeted update messages:

json
{ "type": "liveQueryAddedNode",   "id": "lq-orders", "nodeId": "660e8400-...", "data": { ... } }
{ "type": "liveQueryUpdatedNode", "id": "lq-orders", "nodeId": "550e8400-...", "data": { ... } }
{ "type": "liveQueryRemovedNode", "id": "lq-orders", "nodeId": "550e8400-..." }

Destroy a Live Query

When you no longer need a live query, send destroyLiveQuery to release server-side resources:

json
{
  "type": "destroyLiveQuery",
  "id": "lq-orders"
}

The server responds with liveQueryDestroyed:

json
{
  "type": "liveQueryDestroyed",
  "id": "lq-orders"
}

Error Handling

Errors are returned as error messages:

json
{
  "type": "error",
  "code": "INVALID_PATH",
  "message": "Unknown resource path: invalid/path",
  "requestId": "req-5"
}

See the WebSocket API Reference for the full list of error codes.

Complete Example

typescript
const ws = new WebSocket('wss://core.your-tenant.hantera.cloud/ws')

ws.onopen = () => {
  ws.send(JSON.stringify({ type: 'auth', token: 'your-access-token' }))
}

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data)

  switch (msg.type) {
    case 'authenticated':
      // Subscribe to job events
      ws.send(JSON.stringify({
        type: 'subscribeEvents',
        subscriptions: [{
          id: 'jobs',
          path: 'jobs',
          events: ['jobScheduled', 'jobStarted', 'jobCompleted', 'jobFailed']
        }]
      }))
      break

    case 'event':
      console.log(`[${msg.eventType}]`, msg.data)
      break

    case 'ping':
      ws.send(JSON.stringify({ type: 'pong' }))
      break

    case 'error':
      console.error(msg.code, msg.message)
      break
  }
}

Best Practices

  • Reconnect on disconnect — implement automatic reconnection with exponential backoff. Re-authenticate and re-subscribe after reconnecting.
  • Subscribe selectively — only subscribe to paths and events you need to reduce message volume.
  • Handle overflow warnings — when you receive QUEUE_OVERFLOW, re-sync from the Graph API to recover missed changes.
  • Destroy live queries — call destroyLiveQuery when a query is no longer needed to free server memory.
  • Use request correlation — include requestId in messages to correlate responses.

© 2024 Hantera AB. All rights reserved.