String API
Feed Builder API

Streaming progress

Watch a build over server-sent events, and what resuming can and cannot recover.

GET /v1/builds/{id}/events is a text/event-stream that runs for the life of the build.

const events = new EventSource(`${FEEDBUILDER_API}/v1/builds/${id}/events`);
events.addEventListener("build.state", (e) => render(JSON.parse(e.data)));
events.addEventListener("build.gate.raised", (e) => promptOperator(JSON.parse(e.data)));
events.addEventListener("build.error", (e) => fail(JSON.parse(e.data)));

Generated clients will not handle this endpoint

OpenAPI has no way to describe a stream of framed events, so every generator produces a method that buffers the whole response or times out. Use your language's SSE client here — EventSource, httpx + httpx-sse, launchdarkly/eventsource — and the generated client for the other ten operations.

Frame shape

id: <build version>
event: <name>
data: <one line of JSON>

data is always a single line: the payload is JSON-encoded, which escapes every newline.

Event names

Eventdata
build.stateA full build object. The first frame is always this one.
build.progress{id, version, status, progress?, pending_gate_id?}
build.activityOne activity item.
build.gate.raisedOne gate — answer it at POST /v1/builds/{id}/gates/{gate_id}.
build.errorThe ordinary error envelope.

Names are namespaced so you can register one handler per kind and ignore a kind you do not know. That is what keeps a new event type from being a breaking change.

build.error exists because a stream that dies after the headers are written cannot change its status code. Without a frame saying so you could not tell a finished build from a dropped connection, and you would retry either never or forever.

Because the first frame is read before any header is written, an unknown or foreign build id is a real 404 rather than a 200 that ends immediately.

Heartbeat

A : keep-alive SSE comment goes out every 15 seconds. Comments are ignorable by the protocol, so no client needs to know about it — it is there because a build spends long stretches silent while the builder works, and intermediaries reap connections that go quiet.

Resuming: Last-Event-ID suppresses, it does not replay

id: carries the build's version. Frames with no version of their own — a gate raise, an activity item — are stamped with the last one seen, so Last-Event-ID stays monotonic across every frame type.

A gap in the stream cannot be replayed

The core takes no cursor, so nothing can resend the frames you missed. What a resubscribe gets is a fresh full-state snapshot — current status, pending gate, gate history, messages, transcript — and Last-Event-ID is used only to drop frames you demonstrably already have.

Progress ticks that elapsed while you were disconnected are gone. They are liveness, and the snapshot supersedes them. Gate raises and activity items are not lost, but only because the snapshot re-carries them.

The practical consequence: treat the build.state frame as truth on every (re)connect, and never accumulate state by applying build.progress deltas across a reconnect.

If you would rather poll

GET /v1/builds/{id} returns the same build object the first frame carries. version increases monotonically, so you can cheaply tell whether anything changed. Polling is the right choice for a batch integration that checks in every few minutes; the stream is the right choice for a UI.