Model APIs

Streaming

On this page
  1. Job status over SSE
    1. How it works
    2. Parameters
  2. Event reference
  3. Agent session events
  4. When to use streaming
  5. Error responses
  6. Next steps

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.

Ticket-authorized job status stream The client uses bearer authentication to mint a single-use ticket, opens the job stream with that ticket, receives status changes and a terminal complete event, and the stream closes. Your client Nolgia API POST /jobs/{id}/sse-ticket + bearer 201 {ticket, expires_at} GET /jobs/{id}/sse?ticket=… event: status — current state, then changes event: complete — terminal state The stream closes. A reconnect needs a new single-use ticket.
The client mints a ticket, opens the stream, and receives status and completion events

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.

shell
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>"
shell
# A job SSE command is not available in the CLI; use long-poll instead.
nolgia wait "$JOB_ID"
TypeScriptexample
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);
}
Pythonexample
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)
Rustexample
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"
}
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.

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 #

Parameter In Required Description
id path Yes Job UUID.
Parameter In Required Description
id path Yes Job UUID.
ticket query Yes Single-use ticket from POST /jobs/{id}/sse-ticket.

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.

shell
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 for session creation, turns and event payloads.

When to use streaming #

Use SSE for a live status display or agent conversation. Use long-poll 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 #