Skip to content
Hoody.com

The notification server stores the desktop notifications sent on each display and exposes a read and write API over HTTP and WebSocket. Use these endpoints to read the history of one or more displays, subscribe to new notifications as they arrive, dismiss notifications so they no longer appear in the read API, and clear the dismissed state. All requests run on the notification server’s per-container host and authenticate with a Hoody token.

Retrieves notifications for one display, a comma-separated list of displays, or all.

Without a paging parameter the response holds the newest limit notifications, newest first. With since, after_id, or cursor the response holds the oldest limit matching notifications after that point, oldest first, so a client can catch up page by page: pass data.next_cursor back as cursor while data.has_more is true, and the pages join into one ascending list with every matching notification exactly once. To read the whole history, start with since=0.

Dismissed notifications are left out: a row is hidden when its id was dismissed globally or for its own display. Rows without an id are left out and do not count toward limit.

NameInTypeRequiredDescription
displaypathstringYesA display ID (1 or :1, up to 5 digits), a comma-separated list (1,:2,3), or all. Any invalid element rejects the whole request with 400.
limitqueryintegerNoMaximum number of notifications to return. Default: 100.
sincequeryintegerNoForward start point, Unix milliseconds, inclusive: returns the oldest limit notifications with timestamp >= since, oldest first. Use it to start from a known time (0 for the whole history). Continue with cursor set to data.next_cursor.
after_idqueryintegerNoForward cursor on notification id, exclusive: returns the oldest limit notifications with id > after_id, chosen and listed by id. Ids are numbered per display, so the filter is only meaningful for a single display.
cursorquerystringNoKeyset cursor, exclusive: pass back data.next_cursor from an earlier response to get the oldest limit notifications after it, oldest first. The cursor keeps the order of the request that produced it. Opaque; cannot be combined with since or after_id.
usernamequerystringNoRead only this user’s history files (letters and digits only). Both username and session narrow the read to that user’s and session’s files.
sessionquerystringNoRead only this session’s history files (letters and digits only). Both username and session narrow the read to that user’s and session’s files.
Terminal window
curl -G "https://{projectId}-{containerId}-n-1.node-example-1.containers.hoody.com/api/v1/notifications/all" \
--data-urlencode "limit=50" \
-H "Authorization: Bearer <token>"
{
"success": true,
"data": {
"count": 1,
"displays": ["1"],
"notifications": [
{
"id": 10,
"display_id": 1,
"appname": "Google Chrome",
"summary": "Focus or Open a Window",
"body": "Click to focus the window",
"message": "Focus or Open a Window: Click to focus the window",
"icon_url": "/api/v1/notifications/icons/6_10_1749024932903.png",
"has_icon": true,
"timestamp": 1749024932903,
"timeout": 5000,
"progress": 0,
"category": "system",
"urgency": "NORMAL",
"default_action_key": "default",
"actions": [
{
"key": "default",
"name": "default",
"text": "Open"
}
]
}
],
"next_cursor": "MTc0OTAyNTAyMjk1NS4xNS4x",
"has_more": false
}
}

Streams new notifications as they are written. The same endpoint serves two transports based on the request:

  • WebSocket: send a WebSocket upgrade request. A disallowed Origin (see WS_ALLOWED_ORIGINS) is refused with 403 before the upgrade. Otherwise the server answers 101 Switching Protocols; problems found after that arrive as JSON error frames on the socket, never as HTTP status codes. The per-IP connection limit (WS_MAX_CONNECTIONS_PER_IP) sends an error frame and closes without a status code. Close codes include 1000 (server shutting down), 1001 (heartbeat timeout after two missed pings), and 1008 (message rate limit, WS_MAX_MESSAGES_PER_MINUTE). A send that stalls for more than 10 seconds drops the connection without a close frame. Clients should reconnect with backoff after any close they did not start.
  • Server-Sent Events: any other GET returns 200 text/event-stream. The first event is connected, followed by periodic heartbeat messages. The retry field asks EventSource to reconnect after 3 seconds. The stream ends when the server shuts down.

Both transports send a heartbeat message (WebSocket every WS_HEARTBEAT_INTERVAL ms, SSE every 15 seconds) and a resync message when the client fell behind and notifications were skipped. Re-fetch those with GET /{display}?since=<last seen timestamp>.

displays is required for SSE. For WebSocket it is optional: without it the socket receives nothing until the client sends a subscribe message.

NameInTypeRequiredDescription
displaysquerystringNoComma-separated display IDs (1,:2,3), or all / * for every display. Required for SSE (400 without it). Optional for WebSocket: without it the socket receives nothing until the client sends a subscribe message; an invalid ID arrives as an error frame after the upgrade.
Terminal window
curl -N "https://{projectId}-{containerId}-n-1.node-example-1.containers.hoody.com/api/v1/notifications/stream?displays=all" \
-H "Accept: text/event-stream" \
-H "Authorization: Bearer <token>"

The 101 Switching Protocols response carries no body. The WebSocket frames exchanged on the socket are described in the endpoint description above.

Marks notification IDs as dismissed. Dismissed notifications are left out of later GET responses (and of every query: /all, lists, and single displays).

Without displayId (or with null) the IDs are dismissed globally, on every display. With displayId they are dismissed only on that display, which matters because notification IDs are numbered per display.

NameTypeRequiredDescription
notificationIdsarray of integersYesNotification IDs to dismiss. Must be a non-empty array with at least one integer; non-integer entries are ignored.
displayIdstring or integerNoDisplay to scope the dismissal to: "1", ":1", or 1 (0-99999; surrounding whitespace and extra leading colons are ignored). Omit or send null to dismiss globally. Any other value or type is rejected with 400; it never widens the dismissal to every display.
Terminal window
curl -X POST "https://{projectId}-{containerId}-n-1.node-example-1.containers.hoody.com/api/v1/notifications/dismiss" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"notificationIds":[10,11,12],"displayId":"1"}'
{
"success": true,
"message": "3 notification(s) dismissed"
}

Clears dismissed state so dismissed notifications show again. Without displayId it clears every scope: the global list and every display’s list. With displayId it clears only that display’s list; global dismissals stay.

NameInTypeRequiredDescription
displayIdquerystringNoClear only this display’s dismissals (1 or :1, 0-99999; surrounding whitespace and extra leading colons are ignored). Omit to clear everything. An invalid value is rejected with 400.
Terminal window
curl -X DELETE "https://{projectId}-{containerId}-n-1.node-example-1.containers.hoody.com/api/v1/notifications/dismiss?displayId=1" \
-H "Authorization: Bearer <token>"
{
"success": true,
"message": "Dismissed notifications cleared"
}