Model APIs

Proxy setup

On this page
  1. How it works
  2. Plain Node
  3. Express
  4. Call it from the browser
    1. Parameters reference
  5. What to allow
  6. Error responses
  7. Next steps

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.

A server route keeps the PAT out of the browser The browser calls your local API route. Your server checks an allow-list, adds its bearer token and calls the Nolgia API. The status and response body travel back through your route to the browser. Browser Your server route Nolgia API no PAT allow-list + PAT api.nolgia.ai POST /api/nolgia/jobs/cost POST /v1/jobs/cost Authorization: Bearer … status + response body status + response body
The browser calls your route; your server adds the PAT and calls the Nolgia API, then returns the response

Plain Node #

Save this program as proxy.mjs. It is the fixture exercised against GET /me and POST /jobs/cost on 2026-09-21.

jsproxy.mjs
// 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"));
shell
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.

shell
npm install express
jsexpress-proxy.mjs
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"));
shell
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.

TypeScriptbrowser.ts
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.

JSON200: quote
{
  "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.

JSON202: queued
{
  "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.

JSON200: succeeded
{
  "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.

Next steps #