Model APIs
Synchronous: subscribe
On this page
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.
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>"
nolgia gen image --model flux-pro --prompt "a paper-cut mountain range at dawn" --out first.png
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");
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")
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.
{
"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.
{
"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.
subscribe(client, endpoint, args, {
pollInterval, maxPollTime, onStatus, signal, headers,
});
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:
...
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. |

