Ghosty
API

Events (SSE)

Catalog of a conversation's stream events — started, chunk, tool, permission, artifact, done — and how to write a client that doesn't miss anything.

Updated 2026-09-16

GET /api/v2/me/agents/:agentId/conversations/:sessionId/events is a Server-Sent Events stream with named events (event: chunk), always with data (even if it is {}).

Three rules that surprise people

  1. It is re-subscribable. On connecting you first receive everything the current turn already emitted, then the live feed. An F5, or coming back tomorrow, loses nothing. Disconnecting does not cancel the turn: the work belongs to the server.
  2. At rest you get a done right away, flagged reposo: true ("idle"), so you can tell "no turn" from "a turn finished".
  3. Open permissions are re-sent on every subscription. The server doesn't know which screens are still alive. Dismiss them with permission-resolved, which also covers another client answering.

There is a heartbeat every 25 s so no proxy cuts a long turn for silence.

Catalog

EventdataWhen
started{}Connection established.
caps{ image: boolean }Agent capabilities (whether it sees images).
status{ phase: "waking" | "session" }The machine is waking up / the turn is in session.
chunk{ text, turnId }A piece of the reply. Concatenate them by turnId.
thought{ text }Visible reasoning from the model, if the engine exposes it.
tool{ id, title?, kind?, status, path?, detalle? }Tool lifecycle: pending → in_progress → completed | failed. Same id on every change. detalle = detail.
title{ title }The conversation was named.
models{ options: [{value,name}], current }Available models and the current one.
usage{ used, size, cost, input?, output? }Accumulated session tokens. used may exceed size after a compaction: it is not a context gauge.
permission{ id, title, tool?, options: [{optionId,name,kind?}] }The agent asks for confirmation. Answer with POST …/permission { id, optionId }. Ten minutes without a reply = denied.
permission-resolved{ id }That request was closed (by you, by another client or by timeout).
artifact{ … }The agent delivered something: file, document, sheet. Carries a signed url and a type.
done{ turnId } or { reposo: true }The turn finished / there is no turn.
error{ message }The turn failed. Partial text already received is still valid.

Minimal client in Node

typescript
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}`, Accept: "text/event-stream" } });const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();let buf = "";for (;;) {  const { value, done } = await reader.read();  if (done) break;  buf += value;  let i;  while ((i = buf.indexOf("\n\n")) !== -1) {    const frame = buf.slice(0, i); buf = buf.slice(i + 2);    const event = /^event: (.*)$/m.exec(frame)?.[1];    const data = /^data: (.*)$/m.exec(frame)?.[1];    if (!event || data == null) continue;    handle(event, JSON.parse(data));  }}
python
import json, requestswith requests.get(url, headers={"Authorization": f"Bearer {token}"}, stream=True) 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:            handle(event, json.loads(line[6:])); event = None

Retries

If the connection drops, open it again: you get the replay and carry on. To find out whether the turn finished while you were away, look at ultimoTurno ("last turn") in GET …/conversations/:sessionId.