Notifications
Section titled “Notifications”The Notification Server runs inside your container and sends Linux desktop notifications over HTTP. Each request dispatches through notify-send on the container’s X11 display, the same mechanism Linux desktop notification daemons use.
Capabilities
Section titled “Capabilities”- Display notifications:
notify-senddelivers a Linux desktop notification to a container’s X11 display. - WebSocket streaming: subscribe to a live feed of notifications, filtered server-side per display.
- History: query past notifications for one or more displays, with
limitand time-range filters. - Custom icons: attach an image, retrievable again from the icons endpoint.
- Urgency levels: low, normal, and critical.
- Auto-expiration:
expire_timecontrols how long a notification stays on screen. - Categories: tag a notification with a category and filter on it.
How it works
Section titled “How it works”The Notification Server receives HTTP POST requests and calls notify-send on the specified container display. The display parameter maps to an X11 display identifier: "1" (equivalent to :1), or ":2".
Notifications appear wherever that display is rendered: in a Hoody display viewer session, a VNC session, or any remote desktop client connected to the container.
Display routing
Section titled “Display routing”Every notification you send is routed to a specific X11 display inside the container. The flow is:
- The kit’s HTTP handler calls
notify-send, which speaks D-Bus to thedunstdaemon running on the target display. dunstruns a logging hook that records each delivered notification to a per-display history JSON file in the kit’s notification-history directory.- The kit’s file watcher (inotify on that directory) picks up the new entry and fans it out to any WebSocket subscribers.
You do not need to set up Xvfb, D-Bus, or dunst yourself. A display’s services come up in two ways:
- When you create a terminal or desktop session with a
displayargument: Hoody Terminal boots the X server anddunstthe first time a session for that display is created. - On demand, when a notification is triggered: before dispatching, the notifications kit calls Hoody Terminal to ensure the target display exists (display-ensure, enabled by default). A first notification to a not-yet-running display normally succeeds once the display boots. If ensure cannot return a D-Bus address within the display-ensure timeout (30s by default), and no inherited D-Bus session is available, the trigger returns HTTP 500 instead.
// Creating a terminal session with `display: '1'` is enough. When this// returns, X server + dunst should be ready on :1, so notifications to// display "1" do not need a separate manual display start.await box.terminal.sessions.create({ terminal_id: '1', display: '1', wait_until_display: true,});
await box.notifications.notify.trigger({ display: '1', summary: 'Build complete', body: 'Your deployment finished successfully',});The same is true when you open a display via the URL path (terminal-N?display=N&redirect=display, display-N, etc.): every entry point that brings a display up also brings up its notification daemon.
Manual display startup
Section titled “Manual display startup”If notify.trigger returns:
{ "statusCode": 500, "error": "Internal Server Error", "message": "Failed to send notification"}…then the kit’s automatic display-ensure could not obtain a working D-Bus session for :N. Hoody Terminal either could not bring the display up or did not return a dbus_address. Spawn the display explicitly via the SDK and retry:
// Recovery: bring up display :N (boots Xvfb + dunst) explicitly, then retry.await box.terminal.sessions.create({ terminal_id: 'N', display: 'N', wait_until_display: true,});
await box.notifications.notify.trigger({ display: 'N', summary: '…' });Keeping the session alive is also a latency optimization: after a successful ensure, repeated triggers reuse the display-ensure cache for 60 seconds. Once the cache expires, ensure runs again and is usually quick for an already-running display. If you tear the session down with terminal.sessions.delete(terminal_id), the next trigger may first try a cached D-Bus address, then invalidate it after a notify-send failure and re-run ensure. It can still fail if ensure cannot produce a D-Bus session, or if notify-send keeps failing before the retry deadline.
API Endpoints Summary
Section titled “API Endpoints Summary”All endpoints are relative to your Notification Server URL:
https://PROJECT_ID-CONTAINER_ID-n-1.SERVER.containers.hoody.comTriggering:
POST /api/v1/notifications/notify- Send notification to a container display
Fetching:
GET /api/v1/notifications/{display}- Get notification history for a display
Streaming:
- WebSocket:
wss://.../api/v1/notifications/stream?displays=1,2(ordisplays=all)
Icons:
GET /api/v1/notifications/icons/{iconId}- Retrieve notification icon
Health:
GET /api/v1/notifications/health- Service health check
Send a notification
Section titled “Send a notification”# Send a notification to display 1# All kit commands require container targeting: -c <container-id> (or HOODY_CONTAINER)hoody notifications trigger -c CONTAINER_ID \ --display "1" \ --summary "Build Complete" \ --body "Your deployment finished successfully"
# Get notification history for display 1hoody notifications list "1" -c CONTAINER_IDimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });
// Send notification to display 1await containerClient.notifications.notify.trigger({ display: '1', summary: 'Build Complete', body: 'Your deployment finished successfully',});# Send a notification to display 1curl -X POST "https://PROJECT-CONTAINER-n-1.SERVER.containers.hoody.com/api/v1/notifications/notify" \ -H "Content-Type: application/json" \ -d '{ "display": "1", "summary": "Build Complete", "body": "Your deployment finished successfully" }'
# Get notification history for display 1curl "https://PROJECT-CONTAINER-n-1.SERVER.containers.hoody.com/api/v1/notifications/1?limit=50"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Sends a notification to display 1, then fetches its last 50 history entries. Two independent requests — the history link works whether or not you just triggered a notification.
# Send notification
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT-CONTAINER-n-1.SERVER.containers.hoody.com/api/v1/notifications/notify&method=POST&json={"display":"1","summary":"Build%20Complete","body":"Your%20deployment%20finished%20successfully"}&response=transparent
# Get history
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT-CONTAINER-n-1.SERVER.containers.hoody.com/api/v1/notifications/1?limit=50&method=GET&response=transparent The notification appears on the container’s X11 display 1, visible in any connected display session.
Display parameter
Section titled “Display parameter”The display parameter specifies which X11 display to target inside the container:
| Value | Meaning |
|---|---|
"1" | Display :1 (the primary desktop) |
":1" | Same as above, explicit X11 format |
"2" | Display :2 |
":2" | Same as above, explicit X11 format |
The display field is always a JSON string, even when the value is a number ("1", not 1). A JSON number is rejected with a "display" is required validation error. The field is also required, with no default. It must be a syntactically valid X11 display ID: digits, optionally colon-prefixed ("1", ":2", "10"). The display does not have to be running before the request, because the kit ensures it on demand.
See Display routing for how that happens automatically, and Manual display startup for recovery if ensure cannot obtain a D-Bus session.
Urgency levels
Section titled “Urgency levels”low - Subtle, dismisses quickly:
{ "display": "1", "summary": "Background task finished", "urgency": "low", "expire_time": 3000}normal - Standard notification:
{ "display": "1", "summary": "Build Complete", "urgency": "normal"}critical - Highest urgency; often remains visible until dismissed, depending on daemon policy:
{ "display": "1", "summary": "System Alert", "body": "Immediate action required", "urgency": "critical", "expire_time": 0}WebSocket streaming
Section titled “WebSocket streaming”Monitor notifications in real time. The stream endpoint upgrades to a WebSocket (HTTP 101 Switching Protocols).
const ws = new WebSocket( 'wss://PROJECT_ID-CONTAINER_ID-n-1.SERVER.containers.hoody.com/api/v1/notifications/stream?displays=1');
ws.onmessage = (event) => { const msg = JSON.parse(event.data);
if (msg.type === 'notification') { // msg = { type: 'notification', display: '1', // data: { id, display_id, appname, summary, body, urgency, … } } console.log('New notification on display', msg.display, msg.data.summary); }};The stream sends one notification message per emitted notification. For WebSocket clients the endpoint upgrades to a WebSocket, and the same URL serves a Server-Sent Events fallback for EventSource clients. The upgrade itself signals connection, and the server pushes JSON heartbeat messages to keep the connection alive (the SDK surfaces these via onHeartbeat). The server can still push a JSON error message for an invalid initial display id, a rejected origin, or a connection or rate limit. See the streaming reference for the full message shapes.
Per-display isolation
Section titled “Per-display isolation”The displays query parameter chooses which displays the subscription receives:
displays= value | Behavior |
|---|---|
1 | Only notifications routed to display 1 |
1,2,4 | Notifications routed to any of displays 1, 2, or 4 |
all | All displays, useful for debug subscribers and dashboards |
Per-display subscriptions are filtered server-side: a subscriber to display 1 never receives a notification triggered on display 2, even if both are active in the same container. That lets you put one tab per user on the same container without cross-talk:
// User A's tab: only sees notifications meant for themnew WebSocket('wss://…/api/v1/notifications/stream?displays=10');
// User B's tab: only sees their ownnew WebSocket('wss://…/api/v1/notifications/stream?displays=20');Subscribing to a display does not start that display; only trigger requests ensure displays on demand. Pre-creating sessions with parallel terminal.sessions.create calls is optional: do it when you want lower first-notification latency, or visible desktops ready before the first trigger.
Notification history
Section titled “Notification history”Query past notifications:
# Last 50 notifications on display 1curl "https://.../api/v1/notifications/1?limit=50"
# Time rangecurl "https://.../api/v1/notifications/1?since=1749025000000"
# Multiple displayscurl "https://.../api/v1/notifications/1,2,3?limit=100"Use Cases
Section titled “Use Cases”CI/CD pipeline alerts
Section titled “CI/CD pipeline alerts”Long-running build completes → HTTP POST → notification appears on the container display in a running display session.
System monitoring
Section titled “System monitoring”Server alerts → HTTP → desktop notification on the container’s X11 display, visible in any active display session.
Long-running task completion
Section titled “Long-running task completion”Data exports, video rendering, ML model training, and backup completion can announce themselves when they finish, without polling.
Automated workflow events
Section titled “Automated workflow events”Cron jobs, scheduled tasks, and automation scripts can send notifications to the container display upon completion or failure.
Best Practices
Section titled “Best Practices”Display ID conventions
Section titled “Display ID conventions”Use consistent display IDs (per-user, per-tenant, per-purpose) and document your mapping. For latency-sensitive paths, keep a terminal or desktop session alive for displays you send to often; otherwise the notifications kit ensures the display on demand when a trigger arrives.
Notification quality
Section titled “Notification quality”Write clear, actionable summaries. Put the relevant context in the body, set an urgency level that matches the event, and avoid sending notifications a reader will not act on.
WebSocket for dashboards
Section titled “WebSocket for dashboards”Subscribe only to the displays a dashboard needs, implement reconnection logic, filter client-side if you need to, and show notification history in the UI.
Useful Questions
Section titled “Useful Questions”How do I view the notifications?
Section titled “How do I view the notifications?”Notifications appear on the container’s X11 display. Access the display via a Hoody display viewer session, VNC, or any remote desktop client connected to the container.
Can I send notifications to my phone?
Section titled “Can I send notifications to my phone?”No. This system uses Linux notify-send on a container display, not a mobile push notification service. Notifications go to the container’s X11 display environment, not to mobile devices.
What is the display parameter?
Section titled “What is the display parameter?”It’s an X11 display identifier (e.g., "1" for display :1). It must be syntactically valid (digits, optionally colon-prefixed); the kit ensures that display before sending. If ensure cannot obtain a D-Bus session, or notify-send still fails before the retry deadline, the trigger returns HTTP 500.
What if I send to a display that doesn’t exist?
Section titled “What if I send to a display that doesn’t exist?”The kit first tries to bring the display up automatically (display-ensure). A non-existent display only becomes an error if ensure cannot provide a usable D-Bus session, or if notify-send still fails before the retry deadline. That failure is typically HTTP 500 with {"success": false, "error": "Notification dispatch failed", "details": "No D-Bus session available for display :N. hoody-terminal ensure did not provide dbus_address."}. The recovery path is to spawn the display via terminal.sessions.create({ terminal_id, display, wait_until_display: true }), then retry. See Manual display startup. Stream subscribers to a non-running display receive nothing rather than an error.
Can I use this without an X11 display?
Section titled “Can I use this without an X11 display?”No. The kit calls notify-send, which speaks the freedesktop D-Bus notification protocol, so both an X11 display and a notification daemon (dunst) must be running. Both start automatically: either when you create a terminal or desktop session with a display argument, or on demand when the kit ensures the display before a trigger. You do not install or run them yourself.
Does this work offline?
Section titled “Does this work offline?”Usually. The notification server runs inside your container, so delivery does not require internet access once the container, display services, D-Bus, and the notify-send path are all working.
Troubleshooting
Section titled “Troubleshooting”No D-Bus session available (HTTP 500)
Section titled “No D-Bus session available (HTTP 500)”Cause: The kit’s automatic display-ensure could not obtain a working D-Bus session for the target display. Hoody Terminal either could not bring the display up or returned no dbus_address, so notify-send had no session to dispatch through.
Solution: Spawn the display explicitly via the SDK and retry, as in Manual display startup. Once the terminal session is created with wait_until_display: true, subsequent triggers should not need another manual display start. Failures can still occur if D-Bus or notify-send fails before the retry deadline.
Missing notifications on the WebSocket stream
Section titled “Missing notifications on the WebSocket stream”Cause: The subscriber is filtering for a different display (per-display isolation), or the trigger went to a display that is down.
Solution: Confirm the displays= query parameter on the WebSocket matches the display field in the trigger. To receive everything for debugging, subscribe with displays=all.
WebSocket disconnects
Section titled “WebSocket disconnects”Cause: Network instability or idle timeout. Solution: Implement reconnection with exponential backoff and handle connection errors gracefully. The stream is a WebSocket: the browser handles protocol-level Ping/Pong automatically, and the server also pushes JSON heartbeat messages (surfaced by the SDK via onHeartbeat) you can monitor for liveness.