# WebSocket Protocol — Render Progress Live render progress is pushed to the browser via WebSocket. ## Connection ``` URL: wss://api.flatrender.ir/ws/v1/render/{job_id}?token={jwt} Headers: Sec-WebSocket-Protocol: flatrender.v1 ``` The JWT carries `user_id` + `tenant_id`. The Gateway validates that the caller owns the `job_id`. Connection is closed with code `4403` if not. ## Server → Client messages All server messages are JSON with `type` discriminator. ### `hello` — sent on connect ```json { "type": "hello", "job_id": "uuid", "current_state": { "step": "Rendering", "progress": 47, "current_frame": 470, "total_frames": 720, "started_at": "2026-05-27T10:00:00Z" } } ``` ### `progress` — frequent (max every 500ms) ```json { "type": "progress", "job_id": "uuid", "step": "Rendering", "progress": 67, "current_frame": 482, "total_frames": 720, "eta_seconds": 45, "preview_b64": "data:image/jpeg;base64,/9j/4AAQ...", "active_nodes": 3, "message": null } ``` `preview_b64` is sent at most every 2s (last rendered frame thumbnail, ~5-15 KB). ### `step_change` — pipeline transition ```json { "type": "step_change", "job_id": "uuid", "from_step": "Rendering", "to_step": "Validating", "at": "2026-05-27T10:05:00Z" } ``` ### `frame_repair` — frames being re-rendered ```json { "type": "frame_repair", "job_id": "uuid", "missing_frames": [347, 348, 521], "corrupt_frames": [402], "attempt": 1, "max_attempts": 3 } ``` ### `node_event` — node-related notice ```json { "type": "node_event", "job_id": "uuid", "event": "node_crashed", "node_id": "uuid", "auto_recovered": true, "message": "AE crashed on node-7, work reassigned" } ``` ### `done` — terminal success ```json { "type": "done", "job_id": "uuid", "export_id": "uuid", "output_url": "https://cdn.flatrender.ir/exports/abc.mp4", "thumbnail_url": "https://cdn.flatrender.ir/exports/abc.jpg", "duration_sec": 30, "size_bytes": 14523456, "compute_seconds": 124 } ``` ### `failed` — terminal failure ```json { "type": "failed", "job_id": "uuid", "failed_at_step": "Rendering", "error_message": "AE crashed too many times on this template", "error_code": "AE_REPEATED_CRASH", "refund_issued": true, "trace_id": "uuid" } ``` ### `cancelled` — terminal cancellation ```json { "type": "cancelled", "job_id": "uuid", "cancelled_at": "2026-05-27T10:03:12Z", "progress_when_cancelled": 42 } ``` ### `error` — protocol-level error (kept open) ```json { "type": "error", "code": "RATE_LIMIT", "message": "Too many messages; slow down" } ``` ### `ping` — keepalive ```json { "type": "ping", "t": 1714294800 } ``` Client SHOULD respond with `{"type":"pong","t":1714294800}` within 30s or the server may close the connection. ## Client → Server messages ### `pong` ```json { "type": "pong", "t": 1714294800 } ``` ### `subscribe_snapshot` — also get scene snapshot updates over same socket ```json { "type": "subscribe_snapshot", "snapshot_id": "uuid" } ``` ### `cancel_job` — request cancellation ```json { "type": "cancel_job", "job_id": "uuid" } ``` The server replies with a `cancelled` message once accepted. ## Close codes | Code | Meaning | |-------|------------------------------------------| | 1000 | Normal close (job completed/failed) | | 1001 | Server going away (deploy) | | 1008 | Policy violation (bad message) | | 4401 | Unauthorized (bad/expired JWT) | | 4403 | Forbidden (don't own this job) | | 4404 | Job not found | | 4429 | Rate limited | ## Reconnect strategy (client) - Reconnect with exponential backoff (1s, 2s, 4s, 8s, max 30s) - On reconnect, `hello` carries `current_state` so UI catches up - WebSocket is best-effort; UI should also poll `GET /v1/renders/{id}` if it hasn't received `progress` in > 15s ## Rate limits | Direction | Limit | |-----------|-------| | Server → Client `progress` | Max 2 Hz | | Server → Client total messages | 10 per second | | Client → Server | 5 per second |