Model APIs

Synchronous: subscribe

On this page
  1. Today: submit, then wait
    1. Parameters reference
  2. The helper (next release)
  3. Parameters
    1. TypeScript
    2. Python
    3. Rust
  4. Progress updates
  5. When to use
  6. Error responses
  7. Next steps

The subscribe / submit helpers, shipping in TypeScript and Python 0.1.2, wrap a generation request and status polling. Today, the published clients provide the same submit-and-wait behaviour through the endpoint calls below.

Today: submit, then wait #

The generation endpoint returns a job immediately. The finish() loop holds GET /jobs/{id}/wait open, repeats the wait after 408, and returns the asset when the job succeeds; it reports any other terminal outcome. These are the image programs from the Quick Start, including their client construction, wait loop and error handling.

shell
curl -sS https://api.nolgia.ai/v1/generate/image \
  -H "Authorization: Bearer $NOLGIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"model":"flux-pro","prompt":"a paper-cut mountain range at dawn"}'
curl -sS "https://api.nolgia.ai/v1/jobs/<id from the response>/wait?timeout_seconds=120" \
  -H "Authorization: Bearer $NOLGIA_TOKEN"
curl -sS -o first.png "<asset.signed_url from the finished job>"
shell
nolgia gen image --model flux-pro --prompt "a paper-cut mountain range at dawn" --out first.png
TypeScript
import { writeFile } from "node:fs/promises";
import { createNolgiaClient } from "@nolgia/sdk";

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

async function finish(id: string, timeout: number) {
  for (;;) {
    const { data, response } = await nolgia.GET("/jobs/{id}/wait", {
      params: { path: { id }, query: { timeout_seconds: timeout } },
    });
    if (response.status === 408) continue; // the wait window closed; the job is still running
    if (data?.status === "succeeded" && data.asset) return data.asset;
    throw new Error(`job ${id} ended ${data?.status ?? response.status}`);
  }
}

async function download(url: string, file: string) {
  await writeFile(file, Buffer.from(await (await fetch(url)).arrayBuffer()));
}

const { data: image, error } = await nolgia.POST("/generate/image", {
  body: { model: "flux-pro", prompt: "a paper-cut mountain range at dawn", num_images: 1 },
});
if (error) throw new Error(`${error.title}: ${error.detail ?? ""}`);
await download((await finish(image.id, 120)).signed_url, "first.png");
console.log("image job", image.id, "-> first.png");
Python
import os
import httpx
from nolgia import AuthenticatedClient
from nolgia.api.generate import generate_image, generate_video
from nolgia.api.jobs import wait_for_job
from nolgia.models import GenerateImageRequest, GenerateVideoRequest, ImageModel, Job, VideoModel

client = AuthenticatedClient(base_url="https://api.nolgia.ai/v1", token=os.environ["NOLGIA_TOKEN"])


def finish(job_id, timeout):
    while True:
        response = wait_for_job.sync_detailed(job_id, client=client, timeout_seconds=timeout)
        if response.status_code == 408:  # the wait window closed while the job was still running
            continue
        done = response.parsed
        if isinstance(done, Job) and done.status == "succeeded":
            return done.asset
        raise SystemExit(f"job {job_id} {getattr(done, 'status', done)}")


def download(url, path):
    with open(path, "wb") as f:
        f.write(httpx.get(url).content)


job = generate_image.sync(client=client, body=GenerateImageRequest(model=ImageModel("flux-pro"), prompt="a paper-cut mountain range at dawn"))
if not isinstance(job, Job):
    raise SystemExit(f"refused: {job}")
download(finish(job.id, 120).signed_url, "first.png")
print("image job", job.id, "-> first.png")
Rustsrc/main.rs
use std::num::NonZeroU64;

use nolgia_client::{types, ApiError, 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 prompt: types::GenerateImageRequestPrompt = "a paper-cut mountain range at dawn".parse()?;
    let job = client.generate_image()
        .body_map(|b| b.model("flux-pro").prompt(Some(prompt)).num_images(1u64))
        .send().await?.into_inner();
    let asset = finish(&client, &job.id.to_string(), 120).await?;
    std::fs::write("first.png", reqwest::get(&asset.signed_url).await?.bytes().await?)?;
    println!("image job {} -> first.png", job.id);

    Ok(())
}

/// Long-poll until the job settles. A 408 means the wait window closed while
/// the job was still running: wait again.
async fn finish(client: &nolgia_client::Client, id: &str, timeout: u64)
    -> Result<nolgia_client::types::Asset, Box<dyn std::error::Error>> {
    loop {
        match client.wait_for_job().id(id.parse::<uuid::Uuid>()?).timeout_seconds(timeout).send().await {
            Ok(response) => {
                let job = response.into_inner();
                return match (job.status.as_str(), job.asset) {
                    ("succeeded", Some(asset)) => Ok(asset),
                    (status, _) => Err(format!("job {id} ended {status}").into()),
                };
            }
            Err(ApiError::ErrorResponse(response)) if response.status() == 408 => continue,
            Err(error) => return Err(error.into()),
        }
    }
}

The submit call answers 202 with this captured job. The blocking program continues waiting after this response.

JSON202: queued
{
  "created_at": "2026-09-21T03:33:14.622149Z",
  "id": "4a9b2242-f732-4a83-ba41-17448f5aa15f",
  "modality": "image",
  "model": "flux-pro",
  "status": "queued",
  "updated_at": "2026-09-21T03:33:14.622149Z",
  "user_id": "dad27b53-a85b-4e3d-8fd6-b152c803a27c"
}

The successful wait returns the finished job and its asset. This is the captured production response with the signed URLs shortened; the fixture's prompt is from its lighthouse run.

JSON200: succeeded
{
  "asset": {
    "created_at": "2026-09-21T03:33:21.706691Z",
    "display_name": "Lighthouse on a cliff",
    "expires_at": "2026-09-21T05:00:00Z",
    "favorite": false,
    "has_audio": false,
    "id": "f7bc037c-d7d7-434a-8843-26675574de0d",
    "mime_type": "image/png",
    "modality": "image",
    "model": "flux-pro",
    "prompt": "a lighthouse on a cliff, paper-cut style",
    "signed_url": "https://storage.googleapis.com/nolgia-generations-prod/dad27b53-a85b-4e…",
    "size_bytes": 418584,
    "status": "ready",
    "tags": [],
    "thumbnail_url": "https://storage.googleapis.com/nolgia-generations-prod/dad27b53-a85b-4e…",
    "user_id": "dad27b53-a85b-4e3d-8fd6-b152c803a27c"
  },
  "completed_at": "2026-09-21T03:33:21.805937Z",
  "created_at": "2026-09-21T03:33:14.622149Z",
  "id": "4a9b2242-f732-4a83-ba41-17448f5aa15f",
  "modality": "image",
  "model": "flux-pro",
  "status": "succeeded",
  "updated_at": "2026-09-21T03:33:21.805937Z",
  "user_id": "dad27b53-a85b-4e3d-8fd6-b152c803a27c"
}
Field Type Required Description
id string Yes
modality Modality Yes
model string Yes
status JobStatus Yes
asset Asset No
failure JobFailure No
progress number, nullable No
created_at string Yes
completed_at string, nullable No
Field Type Required Description
id string Yes
signed_url string Yes Time-limited GCS signed URL for download.…
expires_at string Yes Expiry of signed_url.
mime_type string No

Parameters reference #

The image request below is the source of truth for model, prompt, and the optional image count. Choose supported values from Common model arguments.

Field Type Required Description
model ImageModel Yes
prompt string No What to generate.…
num_images integer No How many images to render from this one request. Each one is billed, so four images cost four times one. The ceiling is per-model (image.num_images_max); most models allow four.
Parameter In Required Description
id path Yes Job UUID.
timeout_seconds query No

The helper (next release) #

The following subscribe signatures (TypeScript and Python 0.1.2; the Rust crate keeps its own) document the implementation that is already checked in. They poll GET /jobs/{id}, then read the job's assets; they do not use the long-poll endpoint shown above. These are signatures and call forms, not imports supported by today's published packages.

TypeScriptrepository signature
subscribe(client, endpoint, args, {
  pollInterval, maxPollTime, onStatus, signal, headers,
});
Pythonrepository signature
def subscribe(client: AuthenticatedClient, endpoint: str, arguments: dict, *,
              poll_interval: float = 0.5, max_poll_time: float = 1800.0,
              on_status: Optional[Callable[[StatusUpdate], None]] = None,
              headers: Optional[dict[str, str]] = None) -> GenerationResult:
    ...
Rustrepository call form
subscribe(&client, endpoint, arguments, SubscribeOptions::default()).await?

A successful result contains the terminal job, its job id, a media collection, and url for the first returned media item. Each media entry identifies an asset and its signed URL. See Uploads for asset access, and download a signed URL before it expires.

Result field TypeScript Python / Rust Meaning
Job id jobId job_id The accepted job's id, reusable outside this process.
Terminal job job job The job returned by the API.
Media media media Assets listed for the job, falling back to the inline asset if the asset list is unavailable.
First URL url url The first media URL, or no value when there are no media entries.

Parameters #

TypeScript #

The repository helper takes client, endpoint, args, and an optional options object. Durations are in milliseconds.

Name Type Default Meaning
client NolgiaClient Required The authenticated typed client.
endpoint GenerateEndpoint Required /generate/image, /generate/audio, /generate/video, /generate/3d, or /restore/video.
args unknown Required JSON arguments passed unchanged to the endpoint for validation.
pollInterval number 500 Delay between status reads; finite and greater than zero.
maxPollTime number 1800000 Client wait budget; finite and nonnegative.
onStatus (update: StatusUpdate) => void Unset Receives the initial state and changes; callback exceptions propagate.
signal AbortSignal Unset Stops the client's wait when aborted.
headers Record<string, string> Unset Additional headers on the submission request.

Python #

The repository helper takes client, endpoint, arguments, and keyword-only options. Durations are in seconds.

Name Type Default Meaning
client AuthenticatedClient Required The authenticated client.
endpoint str Required The generation endpoint; it must return a job.
arguments dict Required JSON request body.
poll_interval float 0.5 Delay between status reads; finite and greater than zero.
max_poll_time float 1800.0 Client wait budget; finite and nonnegative.
on_status Callable[[StatusUpdate], None] or None None Receives the initial state and changes; callback exceptions propagate.
headers dict[str, str] or None None Additional headers on the submission request.

Rust #

The repository helper takes &Client, an endpoint string, a serde_json::Value request body and SubscribeOptions.

Name Type Default Meaning
client &Client Required The authenticated client.
endpoint &str Required /generate/image, /generate/audio, /generate/video, /generate/3d, or /restore/video.
arguments serde_json::Value Required JSON request body.
poll_interval Duration Duration::from_millis(500) Delay between status reads.
max_poll_time Duration Duration::from_secs(1800) Client wait budget.
on_status Optional boxed callback None Receives &StatusUpdate; the callback is Send + Sync.
headers Vec<(String, String)> Empty Additional headers on the submission request.

/generate/set returns an output set rather than a job and does not use this helper flow. See Asynchronous: submit and poll.

Progress updates #

The repository callbacks onStatus and on_status receive status and progress, together with the job id, status detail, status message and full job. They run once for the initial submit response and again when one of those status fields changes. Progress may be absent; do not invent a percentage when it is missing.

Today, read the same status information from GET /jobs/{id}. A job can remain queued with status_detail: provider_down or upstream_at_capacity; display status_message while it waits. Streaming describes receiving changes without client polling.

Field Type Required Description
status JobStatus Yes
status_detail string, nullable No Machine-readable refinement of a non-terminal status.…
status_message string, nullable No Human-readable companion to status_detail, safe to render to the customer as-is.…
progress number, nullable No

When to use #

Use a blocking call for a command-line script or a worker that handles one job at a time and needs the finished file before continuing. For a web request or several jobs in parallel, store the accepted job id, return promptly, and follow it from a separate process using Asynchronous: submit and poll.

Error responses #

The TypeScript and Python repository helpers raise NolgiaGenerationError; Rust returns GenerationError. Their code preserves the server's problem or failure code, including unknown future codes. The published endpoint calls expose the problem response on submission and failure.code on a failed job; keep those two stages separate.

Outcome Where to read it What to do
Submit refused HTTP problem code and detail Correct the request or follow the code's retry guidance. A refused submit has no new accepted job.
Job failed failure.code, failure.message, failure.credits_refunded Report the terminal reason and the recorded credit settlement.
Wait window closed HTTP 408 from the wait endpoint Keep waiting for the existing job.
Client wait budget expired Helper error timeout Read the existing job later; the generation continues.
Field Type Required Description
code GenerationErrorCode No Stable refinement of kind, present on every job that failed after this field shipped and derived from the recorded reason for older ones.…
kind string Yes What stopped the job.…
message string Yes Human-readable reason, safe to show the customer as-is. The same text as error.detail.
credits_refunded boolean, nullable No What the credit ledger actually did with this job's credit hold, recorded when the hold settled.…
Value Meaning
out_of_credits the wallet cannot pay for the job. Nothing was submitted and nothing was charged. Top up, or submit a cheaper model or fewer seconds.
rate_limit refused for now, not refused outright - the caller's own concurrency ceiling, or the provider's. Retry later; the same request will be accepted.
prompt_nsfw a content filter refused the request or the result it produced, on safety grounds. Editing the prompt or the reference media is the fix. Whether the credits were refunded is failure.credits_refunded, not this code.
ip_detected a content filter refused it for a real person's likeness or a protected work, rather than for safety. The fix is different from prompt_nsfw - change the reference image or the named subject, not the tone of the prompt - which is why it is its own code.
job_failed the job ran and did not produce an asset for any other reason, including a provider error. The default for an unclassified failure.
timeout the job ran past its time budget, or a GET /jobs/{id}/wait returned before the job reached a terminal status. On a failed job the work is over; on a wait the job may still be running.
validation the request itself is not acceptable - a missing or malformed field, a model that does not support the capability asked of it, a duration the model will not render. Nothing was submitted and nothing was charged. Retrying unchanged will fail identically.
confirmation_rejected the cost confirmation gate did not pass. Either a client showed the customer a quote and the customer declined, or a supplied confirmation token was refused. A submit that carries no confirmation token is never refused for this reason.
canceled the job was canceled by its owner (POST /jobs/{id}/cancel), not broken. It is the code the SDK wait helpers raise for a canceled job; cancellation on the job says what the provider did and what was refunded. A canceled job carries no failure.
job_not_cancellable POST /jobs/{id}/cancel refused (409) because the job already finished, or its finished result is already being delivered. The job will reach succeeded or failed on its own; nothing was changed.
approval_required refused with 402 (title Approval Needed) because the generation would take an agent run past the price its customer approved (the credit_ceiling a guided preset's review card showed, sent with the brief). Nothing was submitted and nothing was charged. The detail names the generation's cost and the new run total; the agent must ask the customer to approve that total before it continues, never retry, split the job or switch models to fit.
run_ended refused with 409 (title Run Ended) because the agent run this generation was started for (the turn its turn-scoped credential names) has already ended: the platform failed, swept or stopped the turn, or it finished and delivered its reply. Nothing was submitted and nothing was charged. The agent must stop working on that run, never retry or switch models; the customer's chat already shows how it ended.

Next steps #