Reference / Realtime
Realtime
Everything in CatzPrint is live. The console, apps and stations each keep one WebSocket open; large lists load over HTTPS and then stay current through events.
Who uses it
| Client | Connects to | Authenticates with |
|---|---|---|
| Console and signed-in apps | wss://…/v1/realtime | A ticket from POST /v1/realtime/ticket, or Authorization: Bearer <session token> on the upgrade |
| Stations (desktop and phone) | wss://…/v1/device/realtime | The station's device token from pairing |
IntegrationsServer-to-server integrations using API keys don't need the socket: send jobs with an SDK and receive webhooks for what happens next.
Connecting
A ticket lasts one minute and is tied to your session, so the session cookie never has to cross a WebSocket proxy. Subscribe to each organization you show; you always receive your own personal events, such as your jobs and notifications.
Browser
// 1. Signed in? Ask for a short-lived ticket (the session cookie or token authenticates this).
const { ticket } = await (await fetch("/v1/realtime/ticket", { method: "POST" })).json();
// 2. Open the socket with it.
const ws = new WebSocket(`wss://api-v1.catzprint.com/v1/realtime?ticket=${ticket}`);
ws.onopen = () => ws.send(JSON.stringify({ type: "subscribe", org: orgId }));
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === "event" && msg.event === "resync") refetchEverything();
else if (msg.type === "event" && msg.event.startsWith("job.")) updateJob(msg.data); // { id, status }
};
// Any operation can be called over the socket by name:
ws.send(JSON.stringify({ type: "call", id: "7", op: "jobs.list", path: { org: orgId }, query: { limit: "50" }, body: {} }));
// → { "type": "result", "id": "7", "data": { "items": [ … ], … } }Messages
Every message is a JSON object with a type. Calls carry your own id, and the matching result or error echoes it back. Send a ping every 25–30 seconds to keep the connection open through proxies.
Protocol
// client → server
{"type":"call","id":"7","op":"jobs.list","path":{"org":"<id>"},"query":{"limit":"50"},"body":{}}
{"type":"subscribe","org":"<org id>"}
{"type":"unsubscribe","org":"<org id>"}
{"type":"ping"}
// server → client
{"type":"hello","user":{…},"session_id":"…"}
{"type":"result","id":"7","data":{…}}
{"type":"error","id":"7","error":{"code":"…","message":"…"}}
{"type":"subscribed","org":"…"}
{"type":"event","topic":"org:…","event":"job.updated","data":{"id":"…","status":"printed"}}
{"type":"pong"}Events
| Topic | Events |
|---|---|
org:<id> | org.updated, team.updated, individuals.updated, location.updated, station.updated { id, connected? }, printer.updated, routing.updated, billing.updated, job.updated / job.printed / job.failed { id, status } |
user:<id> | job.updated, notification.created |
station | config.updated, job.cancelled, printers.updated, station.revoked; stations also receive job and update messages |
Events are small: they say what changed. Fetch the full object if you need more than the status.
Staying in sync
- Load lists over HTTPS first, then apply events as they arrive.
- An event named
resyncon topic*means the server may have missed events for you — refetch whatever you're showing. - If the socket drops, reconnect with a fresh ticket, back off between attempts, and refetch after reconnecting.