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
Code
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" # idempotentcurl -sS -X POST "$ZAROZ_API/me/notifications/read-all" -H "$AUTH"
TypeScript
Code
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.
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.
// 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, requestsurl = 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
Code
# open a ticket in an organizationcurl -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 organizationcurl -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 messagescurl -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
Code
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
Code
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 nowcurl -sS -X DELETE "$ZAROZ_API/support/access-requests/$REQUEST" -H "$AUTH" # withdraw an approval
TypeScript
Code
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".