---
title: "Streaming"
description: "Server-sent events for job status and for agent sessions; what streams, what does not."
---

# Streaming

There is no progressive media output; models return finished files. Server-sent events stream job status and agent session events so your application can react while work is in progress.

## Job status over SSE

### How it works

Call `POST /jobs/{id}/sse-ticket` with your bearer token, then open `GET /jobs/{id}/sse?ticket=…`. A ticket expires after 60 seconds and authorizes exactly one stream connection; the stream uses only the ticket, with no bearer header. It sends the current state immediately, follows changes, then emits `complete` and closes for a terminal job. No client polling is needed.

![The client mints a ticket, opens the stream, and receives status and completion events](../assets/diagrams/job-sse.svg)

Set `JOB_ID` to an accepted job id. These TypeScript, Python and Rust blocks are examples, not live-proven programs. They print the wire frames without assuming that a network chunk is a complete event. The Rust example additionally requires `futures-util` and the `stream` feature on `reqwest`.

```bash tab="curl"
$ curl -sS -X POST "https://api.nolgia.ai/v1/jobs/$JOB_ID/sse-ticket" -H "Authorization: Bearer $NOLGIA_TOKEN"
$ curl -N "https://api.nolgia.ai/v1/jobs/$JOB_ID/sse?ticket=<ticket from the response>"
```

```bash tab="CLI"
# A job SSE command is not available in the CLI; use long-poll instead.
$ nolgia wait "$JOB_ID"
```

```ts tab="TypeScript" title="example"
import { createNolgiaClient } from "@nolgia/sdk";

const nolgia = createNolgiaClient(process.env.NOLGIA_TOKEN!);

const id = process.env.JOB_ID!;
const { data, error } = await nolgia.POST("/jobs/{id}/sse-ticket", {
  params: { path: { id } },
});
if (error) throw new Error(`${error.title}: ${error.detail ?? ""}`);
const response = await fetch(`https://api.nolgia.ai/v1/jobs/${id}/sse?ticket=${encodeURIComponent(data.ticket)}`);
if (!response.ok || !response.body) throw new Error(`stream HTTP ${response.status}`);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
for (;;) {
  const { done, value } = await reader.read();
  if (done) break;
  process.stdout.write(value);
}
```

```python tab="Python" title="example"
import os
import httpx
from nolgia import AuthenticatedClient
from nolgia.api.jobs import create_job_sse_ticket
from nolgia.models import SSETicket

client = AuthenticatedClient(base_url="https://api.nolgia.ai/v1", token=os.environ["NOLGIA_TOKEN"])
job_id = os.environ["JOB_ID"]
ticket = create_job_sse_ticket.sync(job_id, client=client)
if not isinstance(ticket, SSETicket):
    raise SystemExit(f"refused: {ticket}")
with httpx.stream("GET", f"https://api.nolgia.ai/v1/jobs/{job_id}/sse", params={"ticket": ticket.ticket}, timeout=None) as response:
    response.raise_for_status()
    for line in response.iter_lines():
        print(line)
```

```rust tab="Rust" title="example"
use std::io::Write;
use futures_util::StreamExt;
use nolgia_client::ClientBuilder;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = ClientBuilder::new("https://api.nolgia.ai/v1")
        .bearer_token(std::env::var("NOLGIA_TOKEN")?)
        .build()?;
    let id = std::env::var("JOB_ID")?.parse::<uuid::Uuid>()?;
    let ticket = client.create_job_sse_ticket().id(id).send().await?.into_inner();
    let response = reqwest::Client::new()
        .get(format!("https://api.nolgia.ai/v1/jobs/{id}/sse"))
        .query(&[("ticket", ticket.ticket)])
        .send().await?.error_for_status()?;
    let mut stream = response.bytes_stream();
    while let Some(chunk) = stream.next().await {
        std::io::stdout().write_all(&chunk?)?;
        std::io::stdout().flush()?;
    }
    Ok(())
}
```

The ticket response is `201 Created`; this is the captured response with the single-use secret redacted:

```json
{
  "ticket": "…",
  "expires_at": "2026-09-21T03:34:15.423881465Z"
}
```

<!-- gen:fields schema=SSETicket -->
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `ticket` | string | Yes | Opaque single-use secret to pass as the `ticket` query parameter. |
| `expires_at` | string | Yes | Instant after which the ticket can no longer be redeemed. |
<!-- /gen -->

The following three frames are from the production stream. The stream's signed `asset_url` is shortened for display.

```text
event: status
data: {"status":"queued"}

event: status
data: {"status":"running","progress":0}

event: complete
data: {"status":"succeeded","progress":0,"asset_url":"https://storage.googleapis.com/nolgia-generations-prod/dad27b53-a85b-4…"}
```

### Parameters

<!-- gen:params op=createJobSSETicket -->
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | Yes | Job UUID. |
<!-- /gen -->

<!-- gen:params op=streamJobSSE -->
| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| `id` | path | Yes | Job UUID. |
| `ticket` | query | Yes | Single-use ticket from `POST /jobs/{id}/sse-ticket`. |
<!-- /gen -->

> [!NOTE]
> A ticket cannot be replayed. If the connection drops, mint a new ticket and reconnect; the new stream gives you the current state. Job status SSE has no `Last-Event-ID` replay contract.

## Event reference

| Event | Fields | Meaning |
| --- | --- | --- |
| `status` | `status`, optional `progress` | Current state immediately, then updates while `queued` or `running`; `progress` is a number from 0 to 1 when known. |
| `complete` on success | `status: succeeded`, optional `progress`, `asset_url` when available | Terminal success; the stream closes. `asset_url` may be a `gs://` URI or an expired signed URL. Fetch `GET /jobs/{id}` or `GET /assets/{id}` with bearer authentication for a fresh asset `signed_url` before downloading. |
| `complete` on failure | `status: failed`, optional `error`, `failure_kind`, `failure_code`, `credits_refunded` | Terminal failure; read the typed failure and recorded refund result. |
| `complete` on cancellation | `status: canceled`, optional `error` (the plain statement of what the cancel did), `credits_refunded` once the hold settles | The job was canceled with `POST /jobs/{id}/cancel`; read `GET /jobs/{id}` for the full `cancellation`. |

Fields are omitted when there is no value to report. Heartbeats are SSE comment lines, not state changes. The failure frame below is a schema-shaped example of a terminal state, not a production capture:

```text
event: complete
data: {"status":"failed","error":"generation failed","failure_kind":"error","failure_code":"job_failed","credits_refunded":true}
```

Read `GET /jobs/{id}` for the full terminal `Job` and embedded asset. Store its id and download the file; do not retain signed URLs as permanent asset identifiers.

## Agent session events

`GET /agent/sessions/{id}/events` is the only live transport for agent turns. It begins with a `retry:` reconnection hint, puts a monotonic `id:` on each `message` frame, and replays persisted messages from the database when you reconnect with `Last-Event-ID`. It uses bearer authentication, unlike the ticket-based job stream.

```bash tab="curl"
$ curl --fail-with-body -N "https://api.nolgia.ai/v1/agent/sessions/$SESSION_ID/events" \
  -H "Authorization: Bearer $NOLGIA_TOKEN"
# On reconnect, LAST_EVENT_ID is the last id received from a message frame.
$ curl --fail-with-body -N "https://api.nolgia.ai/v1/agent/sessions/$SESSION_ID/events" \
  -H "Authorization: Bearer $NOLGIA_TOKEN" \
  -H "Last-Event-ID: $LAST_EVENT_ID"
```

| Frame detail | Purpose |
| --- | --- |
| `retry:` | Server hint for reconnection delay. |
| `id:` on `message` | Cursor to retain for reconnect. |
| `Last-Event-ID` request header | Resume persisted message replay from the database. |

See [Agent Sessions API](./agent-api.html) for session creation, turns and event payloads.

## When to use streaming

Use SSE for a live status display or agent conversation. Use [long-poll](./jobs.html#long-poll-with-wait) for a worker that simply needs the finished job: it receives a normal JSON response and can repeat a timed-out wait. Neither transport changes the generation, its deadline or its credit hold.

## Error responses

| Operation | Status | Meaning |
| --- | --- | --- |
| Mint ticket | `401` | Missing or invalid bearer token. |
| Mint ticket | `404` | Job not found for this caller. |
| Open stream | `401` | Missing, expired or already-used ticket. |
| Open stream | `403` | The ticket is bound to a different job and has been consumed. Mint a fresh ticket for the correct job. |
| Either job SSE endpoint | `503` | Job streaming is not configured. |

These authentication and stream-availability problems do not carry a `GenerationErrorCode`. A generation failure is delivered as a terminal `complete` event with `failure_code`, rather than changing the already-open stream's HTTP status.

## Next steps

:::cards
- [Asynchronous: submit and poll](./jobs.html): Submit a request and retrieve its asset.
- [Agent Sessions API](./agent-api.html): Follow a persistent agent conversation.
:::
