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:

ParameterDescription
containersComma-separated container IDs
environmentsComma-separated environment IDs
clustersComma-separated cluster identifiers
topicA 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

FieldTypeDescription
topicstringWhat happened — e.g. container.state.changed, container.instance.state.changed, hub.activity.new. See Hub Events.
objectobjectA 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.
contextobjectLinks the notification to hub resources. Fields are null when not applicable.

The object field

The shape of object depends on the topic.

FieldTypeDescription
object.idstringThe 24-character hex ID of the affected resource (e.g. a container ID, instance ID, or activity ID).
object.statestringPresent on state-change topics. The new state (e.g. stopping, starting, running, stopped).
object.state_previousstringPresent 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

FieldTypeDescription
context.hub_idstringThe hub this notification belongs to.
context.labelstring or nullAn optional label for the notification.
context.account_idstring or nullThe account that triggered the activity, if any.
context.environmentsstring[] or nullEnvironment IDs related to this notification.
context.dns_zonesstring[] or nullDNS zone IDs related to this notification.
context.clustersstring[] or nullCluster 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.containersstring[] or nullContainer IDs related to this notification.
context.virtual_machinesstring[] or nullVirtual 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."