Streaming
Orders take seconds to minutes; you should not poll for them in a tight loop. The API pushes lifecycle events two ways: Server-Sent Events on plain HTTP, and a multiplexed WebSocket that the SDKs manage for you.
Server-Sent Events
Three streams, all authenticated with the usual header:
| Stream | Scope | Lifetime |
|---|---|---|
GET /v1/orders/:id/events | One order: snapshot, progress, terminal state. | Closes itself on completed or failed. |
GET /v1/batches/:id/events | One shoot: per-order completions plus aggregate counts. | Closes when every scene has settled. |
GET /v1/events | The account: everything on your key, including intermediate frames. | Infinite — reconnect on drop. |
Every stream opens with a snapshot of the current state before the deltas, and sends a ping every 15 seconds to keep intermediaries from closing the connection. Reconnecting is always safe: the fresh snapshot reconciles whatever happened while you were away, so a dropped connection never loses a terminal state.
WebSocket — via the SDKs
The SDKs use a WebSocket instead of SSE, and hide its lifecycle entirely (ticket authentication, reconnection with backoff, snapshot reconciliation). The design rule: one connection per client. order.wait(), batch.wait() and watch() all filter the same multiplexed socket — following fifty orders costs one connection, not fifty.
for await (const ev of bkbn.watch()) {
if (ev.type === 'order') {
console.log(ev.data.orderUuid, ev.data.status, ev.data.progressPercent)
}
} When a WebSocket cannot connect at all (proxies that strip the Upgrade header), wait() falls back to polling by itself. Mid-stream drops reconnect with backoff, and each reconnection re-delivers a snapshot first.
Event shape
Order events carry the order summary — orderUuid, status (queued, running, completed, failed), progressPercent, progressStage, and outputAssetId once completed. Unknown event types may be added over time; ignore what you don't recognize. The SDKs do this for you with explicit UNKNOWN sentinels.