Zaroz
Notifications and support

Notifications, live streams and support tickets

The three ways Zaroz tells you what is going on, and the one way you talk back. The samples use the helper from Using the Zaroz API. For the events of a Workload's units (and their webhooks) see Events and Webhooks.

Notifications

Platform messages for your account: an invoice, a service that failed, an invitation. They are newest first and paged. unread_count is the total across every page, which is what a badge shows.

curl

TerminalCode
curl -sS "$ZAROZ_API/me/notifications?page=1&page_size=20" -H "$AUTH" curl -sS -X POST "$ZAROZ_API/me/notifications/$NOTIFICATION/read" -H "$AUTH" # idempotent curl -sS -X POST "$ZAROZ_API/me/notifications/read-all" -H "$AUTH"

TypeScript

TypeScriptCode
import { call } from "./zaroz"; const feed = await call("GET", "/me/notifications?page_size=20"); console.log(`${feed.body.unread_count} unread`); for (const n of feed.body.data) console.log(n.kind, n.title, n.href, n.read); await call("POST", "/me/notifications/read-all");

Python

Code
from zaroz import call _, feed = call("GET", "/me/notifications?page_size=20") print(feed["unread_count"], "unread") for n in feed["data"]: print(n["kind"], n["title"], n["href"], n["read"]) call("POST", "/me/notifications/read-all")

Marking a notification read is idempotent. A notification that is not yours answers 404, the same as one that does not exist.

Live streams (server-sent events)

Three routes hold a text/event-stream open so you do not have to poll.

StreamRouteFrames
Your new notificationsGET /me/notifications/streamnotification_created
One service's live stateGET /service-instances/{id}/status/streamstatus, network_metrics, resource_metrics, vm_progress
One ticket's conversationGET /support/tickets/{id}/eventsmessage_sent, message_edited, ...

The server closes each connection after fifteen minutes and the first frame sets the reconnect interval, so reconnect when it ends (a browser EventSource does it by itself). The service stream's status frame carries the whole picture (aggregate run state, the lifecycle actions that make sense right now, the operation in flight, every visible resource with live usage) on connect, whenever a resource changes and on a five-second tick. It answers 404 for a service whose subscription is suspended, terminated, cancelled or failed.

curl

TerminalCode
curl -sS -N "$ZAROZ_API/service-instances/$SERVICE/status/stream" -H "$AUTH" -H "Accept: text/event-stream"

TypeScript

TypeScriptCode
// Node 20+: a plain fetch can read the stream; EventSource cannot send an Authorization header. const res = await fetch(`${process.env.ZAROZ_API}/service-instances/${SERVICE}/status/stream`, { headers: { Authorization: `Bearer ${process.env.ZAROZ_TOKEN}`, Accept: "text/event-stream" }, }); const decoder = new TextDecoder(); let buffer = ""; for await (const chunk of res.body as any) { buffer += decoder.decode(chunk, { stream: true }); let i; while ((i = buffer.indexOf("\n\n")) >= 0) { const frame = buffer.slice(0, i); buffer = buffer.slice(i + 2); const event = /^event: (.*)$/m.exec(frame)?.[1]; const data = /^data: (.*)$/m.exec(frame)?.[1]; if (event === "status" && data) console.log(JSON.parse(data)); } }

Python

Code
import json, os, requests url = f"{os.environ['ZAROZ_API']}/service-instances/{SERVICE}/status/stream" headers = {"Authorization": "Bearer " + os.environ["ZAROZ_TOKEN"], "Accept": "text/event-stream"} with requests.get(url, headers=headers, stream=True, timeout=(10, 60)) as r: event = None for line in r.iter_lines(decode_unicode=True): if line.startswith("event: "): event = line[7:] elif line.startswith("data: ") and event == "status": print(json.loads(line[6:]))

Use a plain HTTP client as above for scripts. A browser EventSource cannot set the Authorization header, so web apps stream through their own back end.

Support tickets

Tickets belong to an organization; every member sees the organization's tickets and may reply to a colleague's. Staff identities are never exposed: replies show as "Support Team".

curl

TerminalCode
# open a ticket in an organization curl -sS -X POST "$ZAROZ_API/organizations/$ORG/tickets" -H "$AUTH" -H "Content-Type: application/json" \ -d '{"subject": "Server will not start", "body": "It restarts in a loop since the last deploy. Service id: …"}' # or in your personal organization curl -sS -X POST "$ZAROZ_API/me/tickets" -H "$AUTH" -H "Content-Type: application/json" -d '{"subject": "…", "body": "…"}' curl -sS "$ZAROZ_API/support/tickets?page_size=20" -H "$AUTH" curl -sS "$ZAROZ_API/support/tickets/$TICKET" -H "$AUTH" # with its messages curl -sS -X POST "$ZAROZ_API/support/tickets/$TICKET/messages" -H "$AUTH" -H "Content-Type: application/json" \ -d '{"body": "Here are the logs you asked for."}'

TypeScript

TypeScriptCode
const opened = await call("POST", `/organizations/${ORG}/tickets`, { subject: "Server will not start", body: "It restarts in a loop since the last deploy.", }); const TICKET = opened.body.ticket.id; await call("POST", `/support/tickets/${TICKET}/messages`, { body: "Here are the logs you asked for." }); const detail = await call("GET", `/support/tickets/${TICKET}`); for (const m of detail.body.messages) console.log(m.author_name, m.body);

Python

Code
_, opened = call("POST", f"/organizations/{ORG}/tickets", { "subject": "Server will not start", "body": "It restarts in a loop since the last deploy.", }) TICKET = opened["ticket"]["id"] call("POST", f"/support/tickets/{TICKET}/messages", {"body": "Here are the logs you asked for."}) _, detail = call("GET", f"/support/tickets/{TICKET}") for m in detail["messages"]: print(m["author_name"], m["body"])

Opening a ticket in /me/tickets always uses your personal organization; there is no organization to get wrong. The list shows a last-message preview, the unread count and the review status. When a ticket is closed you can send a satisfaction review once: POST /support/tickets/{id}/review with problem_solved (boolean), resolution_speed and response_quality (small integers) and an optional comment; only the ticket's owner can.

Giving support access to a service

Support may ask, inside a ticket, for access to one of your services. You decide:

curl

TerminalCode
curl -sS "$ZAROZ_API/support/tickets/$TICKET/access-requests" -H "$AUTH" curl -sS -X POST "$ZAROZ_API/support/access-requests/$REQUEST/approve" -H "$AUTH" curl -sS -X POST "$ZAROZ_API/support/access-requests/$REQUEST/deny" -H "$AUTH" curl -sS "$ZAROZ_API/support/access-grants" -H "$AUTH" # who is inside right now curl -sS -X DELETE "$ZAROZ_API/support/access-requests/$REQUEST" -H "$AUTH" # withdraw an approval

TypeScript

TypeScriptCode
const grants = await call("GET", "/support/access-grants"); for (const g of grants.body) console.log(g.service_instance_id, g.expires_at, g.state); await call("DELETE", `/support/access-requests/${grants.body[0].id}`); // withdraw at any time

Python

Code
_, grants = call("GET", "/support/access-grants") for g in grants: print(g["service_instance_id"], g["expires_at"], g["state"]) call("DELETE", f"/support/access-requests/{grants[0]['id']}") # withdraw at any time

An approved grant is for the one service named on the request and nothing else you own. It starts now and stops on its own: after max_duration_hours, the moment the ticket is closed, or when you withdraw it. GET /support/access-grants answers only live grants, so it is the check for "is anybody inside my infrastructure right now".

Last modified on