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:
- Get a short-lived token - Make a regular HTTPS GET request to the notifications endpoint. The API responds with a temporary token.
- 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/notificationsReplace <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.reconfiguredUsing 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
Authorizationheader has the formatBearer <token>(with a space after "Bearer"). - Verify that your API key has the
apionly-notifications-listencapability. - Make sure the
X-Hub-Idheader 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://(notws://). 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-Idvalue 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.