Model APIs
Callbacks and webhooks
On this page
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.
| 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.
curl --fail-with-body -sS "https://api.nolgia.ai/v1/jobs/$JOB_ID/wait?timeout_seconds=120" \
-H "Authorization: Bearer $NOLGIA_TOKEN"
nolgia wait "$JOB_ID"
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));
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))
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:
{
"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.
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>"
# A job SSE command is not available in the CLI; use long-poll instead.
nolgia wait "$JOB_ID"
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);
}
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)
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:
{
"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:
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.

