Cycle Logo

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.

🍪 Help Us Improve Our Site

We use first-party cookies to keep the site fast and secure, see which pages need improved, and remember little things to make your experience better. For more information, read our Privacy Policy.