SSE Events
This page describes the Server-Sent Events format go-pve-qemu uses for long-running operations and how to consume it from Go.
Endpoints using SSE: install, start, reboot, set/node.
data: {"step":"<stage>","status":"<status>","message":"<message>"}
event: close
data: {}
Notes
- Responses are always HTTP 200; read the events to tell success from failure
message is built by string concatenation without JSON escaping; when command output contains " or \ that line may not parse as JSON, so clients should skip lines that fail to parse
- When a pre-check fails (
ALLOW_IPS, VM state) you get a single data: {"message": "..."} line with no step / status
- If the client disconnects, the server-side flow keeps running and simply stops pushing events
Go Client Example
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
)
type event struct {
Step string `json:"step"`
Status string `json:"status"`
Message string `json:"message"`
}
func main() {
body, err := json.Marshal(map[string]any{
"os": "ubuntu",
"version": "24.04",
"cpu": 2,
"ram": 4096,
"disk": "32G",
})
if err != nil {
log.Fatal(err)
}
resp, err := http.Post("http://192.168.0.11:8080/api/vm/install", "application/json", bytes.NewReader(body))
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Fatalf("install rejected: %s", resp.Status)
}
failed := false
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
if line == "event: close" {
// server-side flow finished
break
}
payload, ok := strings.CutPrefix(line, "data: ")
if !ok {
continue
}
var e event
if err := json.Unmarshal([]byte(payload), &e); err != nil {
// unescaped command output, skip
continue
}
fmt.Printf("[%s] %s %s\n", e.Status, e.Step, e.Message)
if e.Status == "error" {
failed = true
}
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
if failed {
log.Fatal("install failed")
}
}