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.