WebSocket API Reference
This page documents every message type in the Hantera WebSocket protocol, grouped by capability. For connection setup, authentication and reconnection guidance, see WebSocket.
Endpoint: wss://{hostname}/ws
All messages are JSON text frames. Binary frames are not supported.
WARNING
Preview API: The WebSocket API is currently in preview and subject to change before final release.
Contents
| Group | Messages |
|---|---|
| Connection | auth, authenticated, ping, pong |
| Errors and Warnings | error, warning |
| Event Subscription | subscribeEvents, unsubscribeEvents, subscribedEvents, unsubscribedEvents, event |
| Live Queries | createLiveQuery, destroyLiveQuery, liveQueryCreated, liveQueryData, liveQueryAddedNode, liveQueryUpdatedNode, liveQueryRemovedNode, liveQueryBatch, liveQueryDestroyed |
Base Structure
Every message has a type field identifying its kind. Request messages may include a requestId, which the server echoes back on the corresponding response or error.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Message type |
requestId | string | No | Correlation ID, echoed on the response |
Connection
Messages that establish and maintain the connection itself.
auth
Client → Server. Authenticates the connection. Must be the first message sent.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | "auth" |
token | string | Yes | Bearer token without the "Bearer " prefix |
{
"type": "auth",
"token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}Response: authenticated, or error with AUTH_FAILED.
Authentication must complete within 10 seconds or the connection closes with code 4001.
authenticated
Server → Client. Confirms successful authentication. The connection is now ready for event subscriptions and live queries.
| Field | Type | Description |
|---|---|---|
type | string | "authenticated" |
{ "type": "authenticated" }ping
Server → Client. Keep-alive probe, sent every 30 seconds.
| Field | Type | Description |
|---|---|---|
type | string | "ping" |
timestamp | string | ISO 8601 timestamp |
{
"type": "ping",
"timestamp": "2025-12-07T21:30:00.000Z"
}pong
Client → Server. Response to a ping. Must be sent within 30 seconds or the connection closes with code 4002.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | "pong" |
{ "type": "pong" }Errors and Warnings
error
Server → Client. An error response to a request, or a fatal connection error.
| Field | Type | Description |
|---|---|---|
type | string | "error" |
code | string | Machine-readable error code |
message | string | Human-readable description |
requestId | string | Correlation ID from the request, if any |
details | unknown | Additional context |
{
"type": "error",
"code": "INVALID_PATH",
"message": "Unknown resource path: widgets",
"requestId": "req-5"
}Error codes:
| Code | Description | Connection closed? |
|---|---|---|
AUTH_REQUIRED | First message was not auth | Yes |
AUTH_FAILED | Invalid or expired token | Yes |
MESSAGE_TOO_LARGE | Message exceeds the size limit | No |
INVALID_MESSAGE | Malformed JSON or missing required field | No |
UNKNOWN_MESSAGE_TYPE | Unrecognised message type | No |
INVALID_PATH | Subscription path not recognised | No |
INVALID_SCOPE | Invalid event types for the given path | No |
SUBSCRIPTION_NOT_FOUND | unsubscribeEvents referenced an unknown ID | No |
TOO_MANY_SUBSCRIPTIONS | Per-connection event subscription limit reached | No |
TOO_MANY_LIVE_QUERIES | Per-connection live query limit reached | No |
LIVE_QUERY_NOT_FOUND | destroyLiveQuery referenced an unknown ID | No |
FORBIDDEN | Insufficient permissions for the requested data | No |
INTERNAL_ERROR | Unexpected server error | No |
warning
Server → Client. A non-fatal notification, typically indicating backpressure.
| Field | Type | Description |
|---|---|---|
type | string | "warning" |
code | string | Warning code |
message | string | Human-readable description |
subscriptionId | string | Affected subscription ID, if applicable |
Warning codes:
| Code | Description |
|---|---|
QUEUE_OVERFLOW | Messages were dropped due to slow client consumption |
{
"type": "warning",
"code": "QUEUE_OVERFLOW",
"message": "5 events dropped for subscription 'all-jobs' due to slow consumption",
"subscriptionId": "all-jobs"
}WARNING
Dropped events are permanently lost. Re-sync from the Graph API to recover missed changes. For workflows requiring guaranteed delivery, use Rules with webhooks instead.
Event Subscription
Push notifications for things that happen in your system. A subscription pairs a path (which resources) with a list of event types (which changes).
Delivery is best-effort: when a client consumes slower than events are produced, the server drops the oldest and emits a QUEUE_OVERFLOW warning.
Paths and Events
Jobs
| Path | Description |
|---|---|
jobs | All job lifecycle events |
jobs/{jobId} | Events for a specific job |
| Event | Description |
|---|---|
jobScheduled | Job created in pending state |
jobStarted | Job execution began |
jobCompleted | Job finished successfully |
jobFailed | Job execution failed |
Job Definitions
| Path | Description |
|---|---|
job-definitions | Statistics for all job definitions |
job-definitions/{jobDefinitionId} | Statistics for a specific job type |
| Event | Description |
|---|---|
jobStatistics | Live bucket update with aggregated counters |
Job Queues
| Path | Description |
|---|---|
job-queues | All job queue statistics |
job-queues/{queue} | Statistics for a specific queue |
| Event | Description |
|---|---|
jobQueueStatistics | Live bucket update with queue depth and throughput |
Actors
| Path | Description |
|---|---|
actors | All 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.
| Event | Description |
|---|---|
checkpoint | Checkpoint 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.
Ingresses
| Path | Description |
|---|---|
ingresses | Statistics for all ingresses |
ingresses/{ingressId} | Statistics for a specific ingress |
| Event | Description |
|---|---|
ingressStatistics | Live bucket update with request counts and latency |
Egresses
| Path | Description |
|---|---|
egresses | Statistics for all egresses |
egresses/{egressKey} | Statistics for a specific egress |
| Event | Description |
|---|---|
egressStatistics | Live bucket update with call counts and latency |
Egress Hosts
| Path | Description |
|---|---|
egress-hosts | Statistics for all egress hosts |
egress-hosts/{transport} | Statistics for hosts of one transport type |
egress-hosts/{transport}/{host} | Statistics for a specific host |
| Event | Description |
|---|---|
egressHostStatistics | Live bucket update with call counts and latency by host |
subscribeEvents
Client → Server. Subscribes to one or more event streams. Requires an authenticated connection.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | "subscribeEvents" |
requestId | string | No | Correlation ID for response |
subscriptions | EventSubscription[] | Yes | Subscriptions to add |
Subscription fields:
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Client-assigned unique ID for this subscription |
path | string | Yes | Resource path (see Paths and Events) |
events | string[] | Yes | Event types to receive |
{
"type": "subscribeEvents",
"requestId": "req-1",
"subscriptions": [
{
"id": "all-jobs",
"path": "jobs",
"events": ["jobScheduled", "jobStarted", "jobCompleted", "jobFailed"]
}
]
}Response: subscribedEvents or error.
unsubscribeEvents
Client → Server. Removes one or more event subscriptions.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | "unsubscribeEvents" |
requestId | string | No | Correlation ID for response |
ids | string[] | Yes | Subscription IDs to remove |
{
"type": "unsubscribeEvents",
"ids": ["all-jobs"]
}Response: unsubscribedEvents or error.
subscribedEvents
Server → Client. Confirms that subscriptions have been registered.
| Field | Type | Description |
|---|---|---|
type | string | "subscribedEvents" |
requestId | string | Echoed from the request |
subscriptions | SubscriptionConfirmation[] | The registered subscriptions |
{
"type": "subscribedEvents",
"requestId": "req-1",
"subscriptions": [
{ "id": "all-jobs", "path": "jobs", "events": ["jobScheduled", "jobStarted", "jobCompleted", "jobFailed"] }
]
}unsubscribedEvents
Server → Client. Confirms that subscriptions have been removed.
| Field | Type | Description |
|---|---|---|
type | string | "unsubscribedEvents" |
requestId | string | Echoed from the request |
ids | string[] | The removed IDs |
{
"type": "unsubscribedEvents",
"ids": ["all-jobs"]
}event
Server → Client. An event notification for one or more registered subscriptions.
| Field | Type | Description |
|---|---|---|
type | string | "event" |
subscriptionIds | string[] | IDs of the subscriptions that matched |
eventType | string | Specific event type (e.g. jobStarted) |
path | string | Full path of the affected resource |
data | unknown | Event payload (varies by event type) |
timestamp | string | ISO 8601 timestamp of the event |
{
"type": "event",
"subscriptionIds": ["all-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"
}Event data shapes:
jobScheduled
{
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"jobDefinitionId": "sync-inventory",
"scheduledAt": "2025-12-07T21:45:00.000Z",
"parameters": { "source": "api" }
}jobStarted
{
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"jobDefinitionId": "sync-inventory",
"startedAt": "2025-12-07T21:45:01.000Z"
}jobCompleted
{
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"jobDefinitionId": "sync-inventory",
"finishedAt": "2025-12-07T21:45:05.000Z",
"elapsedMs": 4000.5,
"result": { "itemsSynced": 150 }
}jobFailed
{
"jobId": "660e8400-e29b-41d4-a716-446655440001",
"jobDefinitionId": "sync-inventory",
"finishedAt": "2025-12-07T21:46:00.000Z",
"elapsedMs": 1500.0,
"error": "Connection timeout"
}jobStatistics
{
"jobDefinitionId": "sync-inventory",
"bucketTime": "2025-12-07T21:00:00.000Z",
"scheduled": 45,
"successful": 40,
"failed": 2,
"minExecution": 120.5,
"maxExecution": 1250.0,
"avgExecution": 450.3,
"p95Execution": 1180.0
}jobQueueStatistics
{
"queue": "default",
"bucketTime": "2025-12-07T21:00:00.000Z",
"readyDepth": 12,
"busyWorkers": 3,
"workers": 5,
"scheduled": 45,
"completed": 40,
"failed": 2
}checkpoint
{
"checkpointId": "770e8400-e29b-41d4-a716-446655440002",
"actorType": "orders",
"actorId": "550e8400-e29b-41d4-a716-446655440000",
"identityId": "880e8400-e29b-41d4-a716-446655440003",
"timestamp": "2025-12-07T21:47:00.000Z"
}ingressStatistics
{
"ingressId": "my-api",
"transport": "http",
"bucketTime": "2025-12-07T21:00:00.000Z",
"requests": 850,
"successes": 800,
"clientErrors": 30,
"serverErrors": 20,
"minDurationMs": 5.2,
"maxDurationMs": 340.1,
"avgDurationMs": 28.7,
"p95DurationMs": 95.3
}egressStatistics
{
"egressKey": "apps/psp.kustom/kustomApi",
"bucketTime": "2025-12-07T21:00:00.000Z",
"total": 200,
"successful": 195,
"failed": 5,
"minDurationMs": 45.0,
"maxDurationMs": 1200.5,
"avgDurationMs": 210.3,
"p95DurationMs": 580.0
}egressHostStatistics
{
"transport": "https",
"host": "api.example.com",
"bucketTime": "2025-12-07T21:00:00.000Z",
"total": 200,
"successful": 195,
"failed": 5,
"minDurationMs": 45.0,
"maxDurationMs": 1200.5,
"avgDurationMs": 210.3,
"p95DurationMs": 580.0
}Live Queries
WARNING
Experimental: Live queries are an experimental feature. Message shapes and behaviour may change.
A live query runs a Graph API query server-side, streams the initial result set, then pushes incremental updates as the underlying data changes.
The lifecycle is:
createLiveQuery→liveQueryCreated- One or more
liveQueryDatamessages carry the initial result set - Incremental updates arrive as
liveQueryAddedNode,liveQueryUpdatedNode,liveQueryRemovedNodeorliveQueryBatch destroyLiveQuery→liveQueryDestroyed
The Result Window
A live query tracks at most a fixed number of root nodes — the window — which holds the first N results in the query's sort order (default 1 000). When more rows match than the window can hold, capped is true.
The window is maintained as an ordered set. When a new node arrives:
- If it sorts outside the window, it is ignored — no message is sent.
- If it sorts inside the window, it is admitted and the current last node is displaced. Both changes arrive together in a single
liveQueryBatchso the set never appears inconsistent.
To see results beyond the window, narrow the query's filter.
createLiveQuery
Client → Server. Creates a server-side reactive query.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | "createLiveQuery" |
id | string | Yes | Client-assigned unique ID for this live query |
format | string | No | Response format. Default: "default" |
query | GraphQuery | Yes | The graph query to run and watch |
query fields:
| Field | Type | Required | Description |
|---|---|---|---|
edge | string | Yes | Graph edge to query (e.g. "orders") |
filter | string | No | Filtrera filter expression |
orderBy | string | No | Sort order in standard Graph format (e.g. "createdAt desc") |
format values:
| Value | Description |
|---|---|
"default" | Node payloads are JSON objects |
"table" | Node payloads are arrays of column values, with a schema in the first liveQueryData message |
{
"type": "createLiveQuery",
"id": "lq-active-orders",
"query": {
"edge": "orders",
"filter": "status == 'processing'",
"orderBy": "createdAt desc"
}
}Response: liveQueryCreated followed by liveQueryData messages, or error.
destroyLiveQuery
Client → Server. Destroys a live query and releases its server-side resources.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | "destroyLiveQuery" |
id | string | Yes | Live query ID to destroy |
{
"type": "destroyLiveQuery",
"id": "lq-active-orders"
}Response: liveQueryDestroyed or error with LIVE_QUERY_NOT_FOUND.
liveQueryCreated
Server → Client. Acknowledges creation. Followed immediately by one or more liveQueryData messages.
| Field | Type | Description |
|---|---|---|
type | string | "liveQueryCreated" |
id | string | Live query ID, echoed from the request |
totalCount | number | null | Total matching nodes, or null if unknown |
capped | boolean | true if more rows match than the window can hold |
{
"type": "liveQueryCreated",
"id": "lq-active-orders",
"totalCount": 42,
"capped": false
}liveQueryData
Server → Client. A batch of nodes from the initial result set. When hasMore is false the initial load is complete and the query is tracking changes.
| Field | Type | Description |
|---|---|---|
type | string | "liveQueryData" |
id | string | Live query ID |
data | unknown[] | Batch of graph nodes |
hasMore | boolean | false on the final batch |
capped | boolean | true if more rows match than the window can hold |
{
"type": "liveQueryData",
"id": "lq-active-orders",
"data": [
{ "id": "550e8400-...", "status": "processing", "createdAt": "2025-12-07T10:00:00Z" }
],
"hasMore": true,
"capped": false
}liveQueryAddedNode
Server → Client. A node has entered the result set — either newly created, or its data changed to match the filter.
| Field | Type | Description |
|---|---|---|
type | string | "liveQueryAddedNode" |
id | string | Live query ID |
nodeId | string | ID of the node that entered |
data | unknown | The node payload |
{
"type": "liveQueryAddedNode",
"id": "lq-active-orders",
"nodeId": "660e8400-e29b-41d4-a716-446655440001",
"data": { "id": "660e8400-...", "status": "processing", "createdAt": "2025-12-07T11:00:00Z" }
}liveQueryUpdatedNode
Server → Client. A node already in the result set has changed. The payload is the node's complete new state, not a delta.
| Field | Type | Description |
|---|---|---|
type | string | "liveQueryUpdatedNode" |
id | string | Live query ID |
nodeId | string | ID of the changed node |
data | unknown | The node payload |
{
"type": "liveQueryUpdatedNode",
"id": "lq-active-orders",
"nodeId": "550e8400-e29b-41d4-a716-446655440000",
"data": { "id": "550e8400-...", "status": "processing", "total": 299.99 }
}liveQueryRemovedNode
Server → Client. A node has left the result set — either deleted, its data no longer matches the filter, or it was displaced from a capped window.
| Field | Type | Description |
|---|---|---|
type | string | "liveQueryRemovedNode" |
id | string | Live query ID |
nodeId | string | ID of the removed node |
{
"type": "liveQueryRemovedNode",
"id": "lq-active-orders",
"nodeId": "550e8400-e29b-41d4-a716-446655440000"
}liveQueryBatch
Server → Client. Several node changes that must be applied together as one atomic update.
The server coalesces changes into a batch whenever applying them individually would expose an inconsistent intermediate state. The most common case is a capped window swap, where an incoming node displaces the current last node — the removal and the addition arrive in the same message, so the result set never appears one item short.
Apply every entry in updates before re-rendering. Entries are ordered and should be applied in sequence.
| Field | Type | Description |
|---|---|---|
type | string | "liveQueryBatch" |
id | string | Live query ID |
updates | BatchEntry[] | Changes to apply together |
Batch entry fields:
| Field | Type | Description |
|---|---|---|
action | string | "added", "updated" or "removed" |
nodeId | string | ID of the affected node |
data | unknown | The node payload. Omitted when action is "removed" |
Each action has the same meaning as its standalone message: added matches liveQueryAddedNode, updated matches liveQueryUpdatedNode, and removed matches liveQueryRemovedNode.
Example — a capped window swap, where a newly created order sorts ahead of the window's last entry:
{
"type": "liveQueryBatch",
"id": "lq-active-orders",
"updates": [
{
"action": "removed",
"nodeId": "550e8400-e29b-41d4-a716-446655440000"
},
{
"action": "added",
"nodeId": "770e8400-e29b-41d4-a716-446655440002",
"data": { "id": "770e8400-...", "status": "processing", "createdAt": "2025-12-07T12:00:00Z" }
}
]
}TIP
Clients that render a list should treat a batch as a single frame — apply all entries, then re-sort and re-render once. Applying entries one at a time and re-rendering between them reintroduces the flicker the batch exists to prevent.
liveQueryDestroyed
Server → Client. Confirms that a live query has been destroyed and its resources released.
| Field | Type | Description |
|---|---|---|
type | string | "liveQueryDestroyed" |
id | string | The destroyed query ID |
{
"type": "liveQueryDestroyed",
"id": "lq-active-orders"
}Close Codes
| Code | Name | Description |
|---|---|---|
1000 | Normal Closure | Clean disconnect by client |
1001 | Going Away | Server shutting down |
1009 | Message Too Big | Message exceeded size limit |
4001 | Auth Timeout | No auth message within 10s |
4002 | Ping Timeout | No pong received within 30s |
4003 | Max Connections | Connection limit reached |