Model APIs

Callbacks and webhooks

On this page
  1. What the provider callback does
  2. What it changes for you
  3. How to be notified today
    1. Long-poll
    2. Server-sent events
    3. Error responses
  4. What a customer webhook would need
  5. Next steps

There is no customer webhook today. What shipped is a signed provider callback that wakes the poller to read a video job's current state; it does not send a notification to your application.

What the provider callback does #

Nolgia creates a per-job signed callback URL at video submission and gives it to the provider. POST /callbacks/{provider} verifies the provider signature and the Nolgia job token before waking an immediate read from the media proxy. The callback body is never the result: the normal completion and credit-settlement path handles the fresh read. Kling is the first provider with callbacks.

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
A signed provider callback wakes the poller; the app observes completion through wait
Method Path What it does
POST /callbacks/{provider} Receive a render provider's completion callback (a wake-up signal).

This is a provider endpoint, not a route your application needs to call. Duplicate callbacks and callbacks for settled jobs are no-ops; the poller continues to reconcile if callbacks stop arriving.

What it changes for you #

Behavior Before With provider callbacks Evidence
Status reads per render About 15 About 3 with a working callback Measured on production.
Callback-enabled video reconciliation 5-second polling cadence Immediate read on callback; 20-second polling backstop Configuration verified in the poller and measured on production.
Broken callback host Completion depends on polling Still settles within about 26 seconds in the exercised case Measured on production; not a latency guarantee.

Jobs without callbacks keep their 5-second polling cadence. A lower provider-read count does not change your request, job id, result format or cost.

How to be notified today #

Long-poll #

Use this in a worker waiting for the result. GET /jobs/{id}/wait returns a terminal Job, or 408 when its wait window closes; repeat the wait on the same id. The SDK loops below are copied from the proven Quick Start programs.

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()),
        }
    }
}

The terminal response is the same Job returned by a normal status read. Here is the captured successful response:

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"
}
Field Type Required Description
id string Yes
status JobStatus Yes
asset Asset No
failure JobFailure No
progress number, nullable No
created_at string Yes
updated_at string Yes
completed_at string, nullable No

For queued, running and failed examples, see Check status.

Parameters

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

Server-sent events #

Use this for a live status display without client polling. Mint a single-use ticket with your bearer token, then open the ticket-authenticated stream; terminal work produces a complete event. The SDK stream readers below are examples and have not been proven against production. Rust requires futures-util and reqwest's stream feature.

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:

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 recorded stream:

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.

Error responses #

Response Meaning What to do
408 timeout on wait Job did not finish in this wait window. Wait again.
401 on ticket creation Invalid bearer token. Replace the token; do not retry unchanged.
404 on ticket creation Job was not found for the caller. Check the job id and account context.
401 on the stream Ticket is absent, expired or consumed. Mint a new ticket.
403 on the stream Ticket is for a different job. Mint a fresh ticket for the correct job; the mismatched request consumed the previous ticket.
503 on either SSE endpoint Job streaming is unavailable. Use long-poll.
Terminal failed job or complete frame Generation ended with job_failed, timeout, prompt_nsfw or ip_detected. Inspect the typed failure and recorded refund outcome.

What a customer webhook would need #

Customer webhooks are not offered; contact us to ask for them.

Next steps #