Using the Hub Notification WebSocket.

This guide walks you through connecting to the Cycle platform's hub notification pipeline. The pipeline is a real-time, one-way WebSocket that streams events as they happen on your hub.

Prerequisites

  • A client - such as cURL or a programming language like JS or Python
  • A Cycle API token - a bearer token with the apionly-notifications-listen capability
  • Your Hub ID - the 24-character hex ID of the hub you want to monitor (e.g. 851425ade6078e98982acd90)

If cURL

If using cURL the example also uses jq but not required specifically.

You will also need a WebSocket client. While its possible to just use cURL for websockets these days, a client makes things much simpler. This guide shows examples with websocat, but any client that speaks the WebSocket protocol will work.

How It Works

Connecting to the hub notification WebSocket is a two-step process:

  1. Get a short-lived token - Make a regular HTTPS GET request to the notifications endpoint. The API responds with a temporary token.
  2. Open the WebSocket - Make the same GET request again, but this time append the token as a query parameter (?token=<token>). The server upgrades the connection from HTTP to a WebSocket.

Once the WebSocket is open, the server pushes JSON-formatted event messages to you in real time. You do not send anything back - this is a receive-only stream.

Step 1: Request a Token

Use cURL to call the notifications endpoint. You need to include your API token in the Authorization header and your Hub ID in the X-Hub-Id header.

curl -s \
-H "Authorization: Bearer <YOUR_API_TOKEN>" \
-H "X-Hub-Id: <YOUR_HUB_ID>" \
https://api.cycle.io/v1/hubs/current/notifications

Replace <YOUR_API_TOKEN> and <YOUR_HUB_ID> with your actual values.

Custom Cores

Heads up for Custom Core users: We use api.cycle.io in our examples. Don't forget to swap that out with your actual subdomain (e.g., api.yourcore.cycle.io) when copying these snippets.

What you get back

The API returns a JSON response containing a short-lived token:

{
"data": {
"token": "eyJhbGciOi..."
}
}

The data.token value is what you will use in the next step. This token expires quickly, so you should use it within a few seconds of receiving it.

Extracting the token with jq

If you have jq installed, you can extract just the token string in one command:

TOKEN=$(curl -s \
-H "Authorization: Bearer <YOUR_API_TOKEN>" \
-H "X-Hub-Id: <YOUR_HUB_ID>" \
https://api.cycle.io/v1/hubs/current/notifications \
| jq -r '.data.token')
echo "$TOKEN"

This stores the token in a shell variable called TOKEN that you'll use in the next step.

Step 2: Open the WebSocket Connection

Now make a WebSocket connection to the same endpoint, passing the token as the ?token= query parameter. This tells the server to upgrade the connection to a WebSocket instead of returning JSON.

websocat "wss://api.cycle.io/v1/hubs/current/notifications?token=$TOKEN"

Filtering with query parameters

By default the WebSocket delivers every notification on the hub. You can scope it down by adding query parameters to the connection URL:

Parameter

Description

containers

Comma-separated container IDs

environments

Comma-separated environment IDs

clusters

Comma-separated cluster identifiers

topic

A single topic to filter by

These can be combined with &. For example, to watch only container.reconfigured events for two specific containers:

wss://api.cycle.io/v1/hubs/current/notifications?token=$TOKEN&clusters=development&environments=ccc425ade6078e98982acd90&containers=851425ade6078e98982acd90,431425ade6078e98982acd90&topic=container.reconfigured

Using JavaScript (Node.js)

If you'd rather use Node.js, here is a complete script using the built-in WebSocket global (available in Node 22+):

const API_TOKEN = "<YOUR_API_TOKEN>";
const HUB_ID = "<YOUR_HUB_ID>";
async function main() {
// Step 1: Get the short-lived token
const res = await fetch(
"https://api.cycle.io/v1/hubs/current/notifications",
{
headers: {
Authorization: `Bearer ${API_TOKEN}`,
"X-Hub-Id": HUB_ID,
},
},
);
const { data } = await res.json();
console.log("Got token, opening WebSocket...");
// Step 2: Open the WebSocket with the token
const ws = new WebSocket(
`wss://api.cycle.io/v1/hubs/current/notifications?token=${data.token}`,
);
ws.addEventListener("open", () => {
console.log("Connected. Waiting for events...");
});
ws.addEventListener("message", (event) => {
const notification = JSON.parse(event.data);
console.log(
`[${notification.topic}] object: ${notification.object.id}`,
);
console.log(JSON.stringify(notification, null, 2));
});
ws.addEventListener("close", (event) => {
console.log(`Connection closed (code: ${event.code}).`);
});
ws.addEventListener("error", (err) => {
console.error("WebSocket error:", err.message);
});
}
main();

Using Python

import requests
import websockets
import asyncio
import json
API_TOKEN = "<YOUR_API_TOKEN>"
HUB_ID = "<YOUR_HUB_ID>"
async def main():
# Step 1: Get the short-lived token
resp = requests.get(
"https://api.cycle.io/v1/hubs/current/notifications",
headers={
"Authorization": f"Bearer {API_TOKEN}",
"X-Hub-Id": HUB_ID,
},
)
token = resp.json()["data"]["token"]
print("Got token, opening WebSocket...")
# Step 2: Open the WebSocket with the token
url = f"wss://api.cycle.io/v1/hubs/current/notifications?token={token}"
async with websockets.connect(url) as ws:
print("Connected. Waiting for events...")
async for raw_message in ws:
notification = json.loads(raw_message)
print(f"[{notification['topic']}] object: {notification['object']['id']}")
print(json.dumps(notification, indent=2))
asyncio.run(main())
Requires the requests and websockets packages:pip install requests websockets

Step 3: Understanding the Notification Payload

Each message on the WebSocket is a JSON object. It is not the full event; it's a pointer that tells you something happened and gives you enough context to decide whether to fetch full details from the REST API.

Here is an example of what a notification looks like:

{
"topic": "container.state.changed",
"object": {
"id": "69826049a254a9142c12ebb6",
"state": "stopping",
"state_previous": "running"
},
"context": {
"label": null,
"hub_id": "5a14ddd8b6393d0001976f44",
"account_id": null,
"environments": ["68518ade194b15be6bfd0a1e"],
"dns_zones": null,
"clusters": ["production"],
"containers": ["69826049a254a9142c12ebb6"],
"virtual_machines": null
}
}

Field breakdown

Field

Type

Description

topic

string

What happened — e.g. container.state.changed, container.instance.state.changed, hub.activity.new. See Hub Events.

object

object

A reference to the affected resource. Always contains an id, and may include topic-specific fields such as state and state_previous for state-change topics.

context

object

Links the notification to hub resources. Fields are null when not applicable.

The object field

The shape of object depends on the topic.

Field

Type

Description

object.id

string

The 24-character hex ID of the affected resource (e.g. a container ID, instance ID, or activity ID).

object.state

string

Present on state-change topics. The new state (e.g. stopping, starting, running, stopped).

object.state_previous

string

Present on state-change topics. The state the resource was in before this change.

For topics that don't carry state, like hub.activity.new, object will only contain id.

The context object

Field

Type

Description

context.hub_id

string

The hub this notification belongs to.

context.label

string or null

An optional label for the notification.

context.account_id

string or null

The account that triggered the activity, if any.

context.environments

string[] or null

Environment IDs related to this notification.

context.dns_zones

string[] or null

DNS zone IDs related to this notification.

context.clusters

string[] or null

Cluster identifiers related to this notification. Note that these are human-readable cluster names (e.g. "production"), not hex IDs. Only populated on topics where a cluster is meaningful — instance-level events typically leave this null.

context.containers

string[] or null

Container IDs related to this notification.

context.virtual_machines

string[] or null

Virtual machine IDs related to this notification.

Filtering Notifications in Your Code

For further filtering or for code based filtering, here's a simple example in JavaScript:

ws.addEventListener("message", (event) => {
const notification = JSON.parse(event.data);
// Only handle specific topics
if (notification.topic !== "container.reconfigured") {
return;
}
console.log(`New activity: ${notification.object.id}`);
// Filter by context - e.g. only notifications involving containers
if (notification.context.containers) {
console.log(
` Containers: ${notification.context.containers.join(", ")}`,
);
}
});

Or filter by context to only react to notifications involving specific resources:

// Only notifications that involve a specific environment
const MY_ENV_ID = "ccc425ade6078e98982acd90";
if (
notification.context.environments &&
notification.context.environments.includes(MY_ENV_ID)
) {
console.log("Activity in my environment:", notification.object.id);
}

Troubleshooting

"401 Unauthorized" on the token request

  • Double-check that your Authorization header has the format Bearer <token> (with a space after "Bearer").
  • Verify that your API key has the apionly-notifications-listen capability.
  • Make sure the X-Hub-Id header is present and correct.

WebSocket connection closes immediately

  • The token from Step 1 is short-lived. If too much time passes between Step 1 and Step 2, the token may have expired. Request a new one and try again.
  • Make sure you are connecting to wss:// (not ws://). The Cycle API requires TLS.

No events are appearing

  • The WebSocket only sends events when something actually happens on your hub. Try triggering an action:
    • start a container
    • restart an instance
    • update an environment
  • Confirm you are connected to the correct hub by checking the X-Hub-Id value you used.

Connection drops after a period of inactivity

  • WebSocket connections may be closed by the server or by intermediate infrastructure (load balancers, proxies) after a period of inactivity. Your client should handle reconnection - request a new token and open a fresh WebSocket.
Cookies

Cookies Preferences

We run basic, anonymous analytics by default to measure site traffic. By clicking "Accept," you allow additional cookies for advanced app improvements and tailored advertising. Choose what you share by clicking "Customize."