Model APIs

Asynchronous: submit and poll

On this page
  1. How jobs work
    1. Key guarantees
  2. Submit a request
    1. Parameters
  3. Price it first
  4. Check status
    1. Queued
    2. Running
    3. Succeeded
    4. Failed
    5. Parameters
  5. Long-poll with wait
    1. Parameters
  6. Stream status updates
    1. Parameters
  7. Get the result
  8. Cancel a request
  9. List your jobs
  10. Duplicate submissions and Idempotency-Key
  11. Provider callbacks
  12. Sets
  13. Error responses
  14. Next steps

Submit a generation, store its job id, and let your application carry on while the model runs. Submit and poll is the recommended production pattern: a worker can resume following the same durable job after your original request or process ends.

Jobs
Jobs

How jobs work #

The job lifecycle Your app submits a video generation job, the Nolgia API accepts it as queued, runs it, and it ends as succeeded or failed (or canceled, when its owner stops it), after which credits are settled. Your app long-polls the wait endpoint while the job runs. Your app Nolgia API POST /generate/video GET /jobs/{id}/wait 202 Accepted job queued running succeeded failed + failure.code settle: credits released or charged long-poll, up to 120 s per call
Submit a generation, wait while it runs, then read the result
Status What is happening SDK value
queued Accepted and waiting to run; a provider outage or capacity wait can keep it here. queued
running The generation or delivery of its result is in progress. running
succeeded The finished asset is available. succeeded
failed Work ended without a delivered asset; inspect failure and its recorded refund outcome. failed
canceled Its owner canceled it with POST /jobs/{id}/cancel. It is never delivered; cancellation says what the provider did and what happened to the credits. canceled
status_detail Meaning What to show
provider_down The provider is unavailable; the queued job waits for recovery. status_message
upstream_at_capacity The provider is healthy but has no free generation slot; the job waits for capacity. status_message

Both details are non-terminal. Keep the existing job and its credit hold; do not submit replacements. Treat an unfamiliar detail as a plain queued job.

Key guarantees #

Submit a request #

Use this when you want the job id immediately and will follow it later. Image, video, audio and 3D generation return 202 Accepted with a Job; set generation and prompt enhancement have their own response shapes.

Method Path What it does
POST /generate/image Submit an image generation job (asynchronous).
POST /generate/audio Submit an audio generation job (asynchronous).
POST /generate/video Submit a video generation job (asynchronous).
POST /generate/video/enhance-prompt Rewrite a video prompt for the model it will run on (signed in; 1 credit).
POST /generate/set Generate a coordinated set of image Outputs.
POST /generate/3d Turn product or object photos into a 3D model.
POST /remove-background/video Remove a video's background (asynchronous).
POST /restore/video Submit a video restoration/upscale job (asynchronous).
shell
curl --fail-with-body -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"}'
shell
nolgia gen image --model flux-pro --prompt "a paper-cut mountain range at dawn" --no-wait
TypeScript
import { createNolgiaClient } from "@nolgia/sdk";

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

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 ?? ""}`);
console.log(image.id);
Python
import os
from nolgia import AuthenticatedClient
from nolgia.api.generate import generate_image
from nolgia.models import GenerateImageRequest, ImageModel, Job

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


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}")
print(job.id)
Rust
use nolgia_client::{types, 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();
    println!("{}", job.id);
    Ok(())
}

The production capture below is the actual 202 response for a flux-pro image request; its ids and timestamps are intact. The captured prompt differs from the reusable Quick Start prompt above.

JSON
{
  "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"
}
Field Type Required Description
id string Yes
modality Modality Yes
model string Yes
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
created_at string Yes
updated_at string Yes
completed_at string, nullable No

Parameters #

Every submission selects a model and the fields supported by that model. The Common model arguments reference covers the request schemas and catalog capabilities.

Field Type Required Description
confirmation_token string No Optional proof that this exact request and price were shown to the customer, from POST /jobs/cost.…
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.

Price it first #

Call POST /jobs/cost with kind and the matching request body, such as image. The quote uses the same price calculation as submission. It submits nothing, reserves nothing, and charges nothing.

Quote, confirm, submit Your app asks the Nolgia API for a cost quote, shows the price to the customer, and on confirmation submits the generation with the confirmation token. The same pricer holds the same credits. A declined quote submits nothing, and a stale or mismatched token is rejected with 422. Your app Nolgia API POST /jobs/cost show the price, customer confirms POST /generate/* with confirmation_token quote: credits, settings summary, confirmation_token, expires_at customer declines: confirmation_rejected, nothing submitted same pricer, same credits, credits held stale or mismatched token: 422 confirmation_rejected
Confirmation gate: quote the request, show the price, and submit with its token
Field Type Required Description
kind JobCostKind Yes
image GenerateImageRequest No
video GenerateVideoRequest No
audio GenerateAudioRequest No
three_d Generate3DRequest No
set GenerateSetRequest No
Field Type Required Description
kind JobCostKind Yes
model string Yes The model id the quote is for, after defaulting — not necessarily the one you sent.
credits integer Yes Display credits. This is the number the submit will hold, computed by the same pricer the submit path uses, for exactly these settings. It is what to show the customer.
basis one of one_generation, set_run Yes Whether credits covers one generation or a whole set run.
members integer, nullable No Number of set members credits covers. Present only when basis is set_run.
duration_seconds integer, nullable No The billed duration the price is for, on a video quote.
quality string, nullable No The resolved quality tier the price is for, when the model has one.
settings array of JobCostSetting Yes The settings that made this price, in the order a dialog should show them. Human-readable; do not parse it.
balance_credits integer, nullable No The wallet balance this quote was compared against, when one could be read.
sufficient_credits boolean Yes Whether the balance covers credits right now. Advisory — the submit re-checks.
confirmation_token string Yes Short-lived, single-request proof that this price was quoted. Send it back as confirmation_token on the matching generate request. Opaque: do not parse or construct it.
expires_at string Yes After this the token is refused and a submit carrying it fails with confirmation_rejected. Re-quote.

The confirmation_token is optional. If you include one, an expired, malformed, foreign, or mismatched token is refused with 422 and confirmation_rejected. Quote again if you change the request or pass expires_at.

With NOLGIA_TOKEN set, quote the request first, then submit the same body with the confirmation_token the quote returned.

shell
curl -sS https://api.nolgia.ai/v1/jobs/cost \
  -H "Authorization: Bearer $NOLGIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"kind":"image","image":{"model":"gpt-image-2","prompt":"a paper-cut mountain range at dawn"}}'
curl -sS https://api.nolgia.ai/v1/generate/image \
  -H "Authorization: Bearer $NOLGIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-image-2","prompt":"a paper-cut mountain range at dawn","confirmation_token":"<confirmation_token from the quote>"}'

The matching production quote below held no credits. Its opaque confirmation token is redacted.

JSON
{
  "balance_credits": 2573,
  "basis": "one_generation",
  "confirmation_token": "…",
  "credits": 4,
  "expires_at": "2026-09-21T03:43:14.171728407Z",
  "kind": "image",
  "model": "flux-pro",
  "settings": [
    {
      "label": "Model",
      "value": "flux-pro"
    }
  ],
  "sufficient_credits": true
}

Check status #

Use GET /jobs/{id} when a worker or application wants a snapshot without keeping a connection open. Read status, keep polling while it is queued or running, then handle the asset or typed failure.

shell
curl --fail-with-body -sS "https://api.nolgia.ai/v1/jobs/$JOB_ID" \
  -H "Authorization: Bearer $NOLGIA_TOKEN"
shell
nolgia status "$JOB_ID"
TypeScript
import { createNolgiaClient } from "@nolgia/sdk";

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

const { data, error } = await nolgia.GET("/jobs/{id}", {
  params: { path: { id: process.env.JOB_ID! } },
});
if (error) throw new Error(`${error.title}: ${error.detail ?? ""}`);
console.log(data);
Python
import os
from nolgia import AuthenticatedClient
from nolgia.api.jobs import get_job
from nolgia.models import Job

client = AuthenticatedClient(base_url="https://api.nolgia.ai/v1", token=os.environ["NOLGIA_TOKEN"])
job = get_job.sync(os.environ["JOB_ID"], client=client)
if not isinstance(job, Job):
    raise SystemExit(f"refused: {job}")
print(job.to_dict())
Rust
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 job = client.get_job().id(id).send().await?.into_inner();
    println!("{job:?}");
    Ok(())
}

Queued #

JSON
{
  "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.642113Z",
  "user_id": "dad27b53-a85b-4e3d-8fd6-b152c803a27c"
}

Running #

These are the fields that change, taken from the production stream's running frame; this is an excerpt, not a complete Job.

JSON
{"status":"running","progress":0}

Succeeded #

JSON
{
  "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-4…",
    "size_bytes": 418584,
    "status": "ready",
    "tags": [

    ],
    "thumbnail_url": "https://storage.googleapis.com/nolgia-generations-prod/dad27b53-a85b-4…",
    "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"
}

Failed #

A Gemini video failure was observed on 2026-09-20 with the message beginning “Video generation failed due to an internal server issue…”. There is no saved failed-job response fixture: this excerpt is built from JobFailure, using that observed message and refund, rather than presented as a captured full response.

JSON
{
  "status": "failed",
  "failure": {
    "kind": "error",
    "code": "job_failed",
    "message": "Video generation failed due to an internal server issue…",
    "credits_refunded": true
  }
}
Field Type Required Description
id string Yes
modality Modality Yes
model string Yes
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.…
asset Asset No
error Error No
failure JobFailure No
progress number, nullable No
created_at string Yes
completed_at string, nullable No
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.…

Parameters #

Parameter In Required Description
id path Yes Job UUID.

Long-poll with wait #

Use long-poll when you want one HTTP request to return the finished job. GET /jobs/{id}/wait holds the connection until the job reaches a terminal state or the requested window closes; the SDK examples reuse the Quick Start's finish() loop. Set JOB_ID to the accepted job id.

shell
curl --fail-with-body -sS "https://api.nolgia.ai/v1/jobs/$JOB_ID/wait?timeout_seconds=120" \
  -H "Authorization: Bearer $NOLGIA_TOKEN"
shell
nolgia wait "$JOB_ID"
TypeScript
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}`);
  }
}

console.log(await finish(process.env.JOB_ID!, 120));
Python
import os
from nolgia import AuthenticatedClient
from nolgia.api.jobs import wait_for_job
from nolgia.models import Job

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)}")



print(finish(os.environ["JOB_ID"], 120))
Rust
use nolgia_client::{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 asset = finish(&client, &std::env::var("JOB_ID")?, 120).await?;
    println!("{}", asset.signed_url);
    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()),
        }
    }
}

A successful wait returns the same succeeded Job shown above, including asset; a failed terminal job is also returned as a Job, with failure. The response fields are the Job and JobFailure tables above.

Parameters #

Parameter In Required Description
id path Yes Job UUID.
timeout_seconds query No

The server accepts a wait window up to 900 seconds.

Stream status updates #

Use SSE for status changes without client polling. Mint a ticket using your bearer token, then open the stream with only that ticket. The ticket is single-use and expires after 60 seconds; reconnect by minting another ticket. These TypeScript and Python stream examples print the frames and have not been proven against production.

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>"
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)

The ticket response is 201 Created:

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 stream answers 200 with Content-Type: text/event-stream. These are the three captured production frames, with the signed URL shortened:

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…"}
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.

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.

Read Streaming for the full stream examples and agent session events.

Get the result #

A succeeded job embeds its asset, shown in the succeeded response above. Download asset.signed_url; metadata describes the returned media, and enhanced_prompt records the composed prompt when available.

Field Type Required Description
id string Yes
enhanced_prompt string, nullable No Image generations only: the server-composed prompt that was actually rendered, present only when it differs from prompt (the Aura layer enhanced the prompt, or a character's or element's canonical description was folded in).…
signed_url string Yes Time-limited GCS signed URL for download.…
expires_at string Yes Expiry of signed_url.
mime_type string No
width integer, nullable No
height integer, nullable No
duration_seconds number, nullable No Media duration in seconds for video/audio assets.…
has_audio boolean, nullable No Whether the asset's media carries an audio stream.…
thumbnail_url string, nullable No Time-limited signed URL for a server-generated thumbnail (image downscale or video poster frame).…

Cancel a request #

POST /jobs/{id}/cancel cancels a queued or running job. The job moves to canceled at once and is never delivered: no asset is added to your library, even if the model provider finishes the render afterwards. The response is the job, and its cancellation object says plainly what happened:

Where the job was What Nolgia does Credits
Not yet at the provider (stage: before_submit): queued, or waiting for a provider slot Nothing is sent Refunded in full now (settlement: refunded)
At the provider, and the provider stops it before it starts (provider_cancel: cancelled) Asks the provider to stop it Refunded in full now
At the provider, and the provider stops it part way and bills only what it rendered (stopped_partway) Asks the provider to stop it The rendered share is charged, the rest refunded (partially_refunded)
At the provider, and it cannot be stopped (requested, refused, unsupported, failed) Asks the provider to stop it where it can, then waits for its final answer settlement: pending until then: refunded if the provider stops or fails the render without billing, charged if it finishes and bills

cancellation.message (also served as status_message) is the customer-facing sentence; show it as written. It never claims a refund before the ledger has made it, and a pending settlement is rewritten when the provider answers. credits_refunded and credits_charged carry the amounts once settled.

Canceling twice returns the same record (200). A job that already finished, or whose finished result is already being delivered, answers 409 with code: job_not_cancellable. Another account's job is a 404. In an organization, members may cancel their own jobs and owners and admins any job; viewers and billing contacts get 403. An agent credential may cancel what its owner may.

Parameter In Required Description
id path Yes Job UUID.

To stop an agent turn instead, use POST /agent/sessions/{id}/interrupt; see Agent Sessions API.

List your jobs #

Use GET /jobs to recover accepted work or group generations by their agent session. The response is a JobPage with items, total, and an optional next_cursor.

Parameter In Required Description
limit query No Maximum items to return.
cursor query No Opaque pagination cursor returned by a prior response.
status query No
modality query No
agent_session_id query No Return only jobs whose agent turn ran in the given chat session (jobs.agent_session_id, stamped at submit). Scoped to the caller's own jobs. Jobs created before this attribution shipped, and generations launched outside an agent turn, are not matched.
shell
curl --fail-with-body -sS "https://api.nolgia.ai/v1/jobs?status=running&modality=video" \
  -H "Authorization: Bearer $NOLGIA_TOKEN"
Field Type Required Description
items array of Job Yes
next_cursor string, nullable No
total integer Yes Total number of jobs matching the status/modality filter, ignoring pagination.

Duplicate submissions and Idempotency-Key #

The same body with the same Idempotency-Key, or with no key both times, inside five minutes returns 409 Conflict and names the earlier job_id. A different key deliberately starts another generation and returns 202. A 409 is never billed.

shell
curl --fail-with-body -sS https://api.nolgia.ai/v1/generate/image \
  -H "Authorization: Bearer $NOLGIA_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: image-take-2" \
  -d '{"model":"flux-pro","prompt":"a paper-cut mountain range at dawn"}'
shell
nolgia --idempotency-key image-take-2 gen image --model flux-pro --prompt "a paper-cut mountain range at dawn" --no-wait
TypeScript
import { createNolgiaClient } from "@nolgia/sdk";

const nolgia = createNolgiaClient(process.env.NOLGIA_TOKEN!, undefined, { headers: { "Idempotency-Key": "image-take-2" } });

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 ?? ""}`);
console.log(image.id);
Python
import os
from nolgia import AuthenticatedClient
from nolgia.api.generate import generate_image
from nolgia.models import GenerateImageRequest, ImageModel, Job

client = AuthenticatedClient(base_url="https://api.nolgia.ai/v1", token=os.environ["NOLGIA_TOKEN"], headers={"Idempotency-Key": "image-take-2"})


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}")
print(job.id)
Rust
use nolgia_client::{types, 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")?)
        .idempotency_key("image-take-2")
        .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();
    println!("{}", job.id);
    Ok(())
}

The duplicate response from production:

JSON
{
  "detail": "this exact request was already submitted as job 35b6ff8d-0c78-440b-bdb3-bfe476b56d76 less than 5m0s ago and has not been billed twice — check it with GET /jobs/35b6ff8d-0c78-440b-bdb3-bfe476b56d76. To run it again anyway, resubmit with a different Idempotency-Key header.",
  "job_id": "35b6ff8d-0c78-440b-bdb3-bfe476b56d76",
  "request_id": "localhost/7bnUUXx7b2-007291",
  "status": 409,
  "title": "Conflict",
  "type": "about:blank"
}
Field Type Required Description
code string No Machine-readable error code.…
type string Yes A URI reference identifying the problem type.
title string Yes
status integer Yes
detail string No
instance string No
job_id string No The job this refusal points at, so a client can follow it without reading detail.…
request_id string No

The supplied comparison capture records an accepted new submission; only the job identity and state fields are shown:

JSON
{
  "created_at": "2026-09-21T03:33:52.474923Z",
  "id": "ecf04b6a-d5a1-4ef6-9afd-22b3c10f8e9e",
  "modality": "image",
  "model": "flux-pro",
  "status": "queued"
}
Field Type Required Description
id string Yes
modality Modality Yes
model string Yes
status JobStatus Yes
created_at string Yes

Provider callbacks #

Jobs without a provider callback use a 5-second polling cadence. Callback-enabled video jobs use a 20-second backstop: a verified provider callback wakes an immediate status read, and the poller reconciles if a callback is lost. Kling is the first callback provider. Measured on production, working callbacks reduced reads per render from about 15 to about 3; a broken callback host still settled within about 26 seconds. These measurements are observations, not response-time guarantees.

Callback as a wake-up signal Your app submits a job and waits on the long-poll. The Nolgia API dispatches it to the provider and uses a 20-second polling backstop for callback-enabled video jobs. When the provider sends a signed callback naming the job, the API does one status read and settles normally, and the app's wait call returns. Your app Nolgia API Provider Kling first; the poller still reconciles POST /generate/* GET /jobs/{id}/wait returns: app notified long-poll open job running one status read, normal settle provider renders dispatch 20 s backstop callback: signed, names the job
The provider callback wakes one status read; the poller remains a backstop

Read Callbacks and webhooks for the boundary between provider callbacks and customer notifications.

Sets #

POST /generate/set answers 202 with an OutputSet, not a job. Each member runs as its own image job with its own credit hold. Poll GET /sets/{id} to follow the set.

Status Meaning
generating Member jobs are still running.
ready All Outputs are ready.
partial Some member jobs failed.
failed All member jobs failed.

If a later member is refused, accepted members keep running and further members are not submitted. Read problems for the refused labels. An Idempotency-Key on the set endpoint is ignored per member.

Error responses #

A refused request has an HTTP problem body; an accepted job that later fails has failure.code on the job. Do not mistake the HTTP 200 that reads a failed job for a successful generation.

Where HTTP status Code Action
Invalid generation request 400 validation Correct the field, model or unsupported capability.
Empty wallet or exceeded credit budget 402 out_of_credits Top up or reduce the request's cost.
Duplicate body and key 409 No code; job_id identifies the existing job Follow that job.
Rejected quote confirmation 422 confirmation_rejected Get a fresh quote for the exact request.
Submit-time content-policy refusal 422 prompt_nsfw or ip_detected Change the prompt or references. These codes can also occur later on a failed job.
Submit-time upstream refusal or failure 422, 502 or 500 job_failed Read the problem and retry safely when appropriate.
Concurrency or account quota 429 rate_limit Wait before resubmitting; use Retry-After-Reset on quota responses.
Wait window elapsed 408 timeout Wait again; this does not fail the job.
Job reaches its deadline or fails during execution Job read, not a submit refusal timeout, job_failed, prompt_nsfw, ip_detected in failure.code Inspect failure.message and failure.credits_refunded.
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 #