Model APIs
Proxy setup
On this page
Browser source is public, so your PAT must stay on your server. A small proxy adds the bearer token and forwards an allow-list of routes, letting the browser quote a generation, submit it and follow its job without receiving the token.
How it works #
Your browser calls a route on your own origin under /api/nolgia. Your server checks that the upstream path is allowed, attaches NOLGIA_TOKEN from its environment, and sends the request to the Nolgia API. The server passes the upstream status and response body back to the browser.
Plain Node #
Save this program as proxy.mjs. It is the fixture exercised against GET /me and POST /jobs/cost on 2026-09-21.
// A key-keeping proxy: the browser calls this server, this server calls Nolgia with the PAT.
import { createServer } from "node:http";
const UPSTREAM = "https://api.nolgia.ai/v1";
const ALLOWED = [/^\/generate\/(image|video|audio|3d)$/, /^\/jobs\/[0-9a-f-]+(\/wait)?$/, /^\/jobs\/cost$/, /^\/me$/];
createServer(async (req, res) => {
const url = new URL(req.url, "http://localhost");
const path = url.pathname.replace(/^\/api\/nolgia/, "");
if (!ALLOWED.some((rule) => rule.test(path))) {
res.writeHead(404).end();
return;
}
const upstream = await fetch(UPSTREAM + path + url.search, {
method: req.method,
headers: { Authorization: `Bearer ${process.env.NOLGIA_TOKEN}`, "Content-Type": req.headers["content-type"] ?? "application/json" },
body: req.method === "GET" || req.method === "HEAD" ? undefined : req,
duplex: "half",
});
res.writeHead(upstream.status, { "Content-Type": upstream.headers.get("content-type") ?? "application/json" });
res.end(Buffer.from(await upstream.arrayBuffer()));
}).listen(3115, () => console.log("proxy on http://localhost:3115/api/nolgia"));
export NOLGIA_TOKEN=nol_...
node proxy.mjs
Express #
Use this equivalent route in an Express server. Save it as express-proxy.mjs; this fixture was exercised against the same two endpoints on 2026-09-21.
npm install express
import express from "express";
const app = express();
app.use(express.json());
const UPSTREAM = "https://api.nolgia.ai/v1";
app.all(/^\/api\/nolgia\/(generate\/(image|video|audio|3d)|jobs\/cost|jobs\/[0-9a-f-]+(\/wait)?|me)$/, async (req, res) => {
const path = req.path.replace(/^\/api\/nolgia/, "");
const query = new URL(req.originalUrl, "http://localhost").search;
const upstream = await fetch(UPSTREAM + path + query, {
method: req.method,
headers: { Authorization: `Bearer ${process.env.NOLGIA_TOKEN}`, "Content-Type": "application/json" },
body: req.method === "GET" ? undefined : JSON.stringify(req.body),
});
res.status(upstream.status).type("application/json").send(Buffer.from(await upstream.arrayBuffer()));
});
app.listen(3115, () => console.log("proxy on http://localhost:3115/api/nolgia"));
export NOLGIA_TOKEN=nol_...
node express-proxy.mjs
Call it from the browser #
Serve this browser code from the same origin as your proxy, or route /api/nolgia to that server in your application's development server. It quotes the exact image request, asks for confirmation, passes the returned token to the submit call, and repeats the wait when a 408 closes the wait window. The type-only import adds API types to your build and sends no PAT to the browser.
import type { components } from "@nolgia/sdk";
type Job = components["schemas"]["Job"];
type Problem = components["schemas"]["Error"];
type Quote = components["schemas"]["JobCostQuote"];
async function finish(id: string, timeout: number) {
for (;;) {
const response = await fetch(`/api/nolgia/jobs/${id}/wait?timeout_seconds=${timeout}`);
if (response.status === 408) continue; // the wait window closed; the job is still running
if (!response.ok) {
const error: Problem = await response.json();
throw new Error(`${error.title}: ${error.detail ?? ""}`);
}
const data: Job = await response.json();
if (data?.status === "succeeded" && data.asset) return data.asset;
throw new Error(`job ${id} ended ${data?.status ?? response.status}`);
}
}
const image = {
model: "flux-pro",
prompt: "a paper-cut mountain range at dawn",
num_images: 1,
};
const quoteResponse = await fetch("/api/nolgia/jobs/cost", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ kind: "image", image }),
});
if (!quoteResponse.ok) {
const error: Problem = await quoteResponse.json();
throw new Error(`${error.title}: ${error.detail ?? ""}`);
}
const quote: Quote = await quoteResponse.json();
if (!window.confirm(`Generate this image for ${quote.credits} credits?`)) {
throw new Error("Generation declined");
}
const submitResponse = await fetch("/api/nolgia/generate/image", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...image, confirmation_token: quote.confirmation_token }),
});
if (!submitResponse.ok) {
const error: Problem = await submitResponse.json();
throw new Error(`${error.title}: ${error.detail ?? ""}`);
}
const job: Job = await submitResponse.json();
const asset = await finish(job.id, 120);
console.log(asset.signed_url);
The quote response is captured production JSON; the confirmation token is redacted. Use the actual token from your own quote when you submit.
{
"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
}
| 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 submit call returns a job immediately. These captured job responses come from the fixture's lighthouse prompt rather than the browser example's mountain prompt.
{
"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 wait returns a terminal job. A successful response contains the finished asset; the signed URLs here are shortened for display.
{
"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 |
Parameters reference #
The browser sends the same request fields as a direct client. The proxy removes /api/nolgia from its local path and preserves query parameters, including the wait window.
| 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 |
|---|---|---|---|
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. |
| Parameter | In | Required | Description |
|---|---|---|---|
id |
path | Yes | Job UUID. |
timeout_seconds |
query | No |
What to allow #
Keep the allow-list limited to the operations your application needs. The two programs above forward these route shapes; they do not proxy the whole API.
| Route | Why the examples forward it |
|---|---|
POST /jobs/cost |
Show the exact credit quote before generation. |
POST /generate/image, POST /generate/video, POST /generate/audio, POST /generate/3d |
Submit one of the supported generation modalities. |
GET /jobs/{id} |
Read the accepted job's current state. |
GET /jobs/{id}/wait |
Wait for a terminal state, within the requested wait window. |
GET /me |
Inspect the upstream account and generation limits during integration. |
| Keep outside the browser proxy | Why |
|---|---|
/pat |
Token management belongs in a trusted account flow. |
/billing/* |
Billing operations should not inherit a shared server PAT through a generic route. |
/organizations/* |
Workspace administration requires its own authorization decisions. |
The fixtures forward the content type and server-side bearer token. They do not forward browser-supplied Authorization, Idempotency-Key, X-Nolgia-Surface, or request-id headers; add an explicit server policy if your product needs those headers. They buffer the upstream response, so use the wait endpoint here; these programs are not SSE stream proxies.
Error responses #
An unlisted route receives 404 from the proxy with an empty body. An allowed route preserves the upstream HTTP status and body: 400 / validation means the generation arguments need correction, 401 means the server's token is invalid, and 408 from the wait endpoint means the job is still running. See Errors for other submit refusals and terminal job failures.

