# Getting started with the Nolgia API

Nolgia is one REST API for image, video and audio generation, the same API
that powers [nolgia.ai](https://nolgia.ai), the `nolgia` CLI, the MCP
server and the NOLGIA Agent. This page is the ten-minute version: what to
install, how to authenticate, how a generation moves from request to asset,
how credits are charged, how to get your own media in, and what an error
looks like. The full contract is the [API reference](../api/) and the raw
[OpenAPI spec](../api/openapi.yaml).

Base URL: `https://api.nolgia.ai/v1`. Staging: `https://api.stg.nolgia.ai/v1`
(separate accounts and tokens).

## One API, three clients

Every client is generated from the same `openapi.yaml`, so the endpoints,
parameters and response shapes are identical whichever you pick.

| Client | Install | Notes |
| --- | --- | --- |
| Python `nolgia` | `pip install nolgia` | httpx-based; `sync` and `asyncio` variants of every call |
| TypeScript `@nolgia/sdk` | `npm install @nolgia/sdk openapi-fetch` | typed `openapi-fetch` client, ESM, Node 18+ and browsers |
| Go `nolgia` | `go get github.com/nolgiainc/nolgia-api/sdk/go` | private repository today: needs `GOPRIVATE=github.com/nolgiainc/*` and repo access |
| `curl` | nothing | every example below works with plain HTTP |

The Rust CLI (`cargo install nolgia-cli`, binary `nolgia`) and the MCP server
sit on the same API; see [nolgia.ai/cli](https://nolgia.ai/cli) and
[nolgia.ai/mcp](https://nolgia.ai/mcp).

## Authenticate

Requests carry a Personal Access Token as a bearer token. Tokens start with
`nol_`. Create one in the app at
[nolgia.ai/settings/api-tokens](https://nolgia.ai/settings/api-tokens); the
plaintext token is shown once. (A signed-in session can also mint one with
`POST /pat`.)

```bash
curl https://api.nolgia.ai/v1/me \
  -H "Authorization: Bearer nol_..."
```

A token confers exactly the permissions of the account that minted it: plan
limits and model entitlements apply to API calls the same way they apply in
the app. Generations made with a personal token draw on the account's shared
top-up credits (see Credits below).

## The job model

Generation is asynchronous. `POST /generate/image`, `POST /generate/video`
and `POST /generate/audio` answer `202 Accepted` with a **job**:

```json
{
  "id": "6f1c2c0e-2a4b-4b53-9d9f-1c0b3e4f5a6b",
  "modality": "image",
  "model": "gpt-image-2",
  "status": "queued",
  "created_at": "2026-09-18T20:15:04Z",
  "updated_at": "2026-09-18T20:15:04Z"
}
```

A job is `queued`, then `running`, then one of `succeeded`, `failed` or
`canceled`. The simplest way to follow it is the long-poll endpoint, which
holds the connection until the job settles or `timeout_seconds` (default 300,
maximum 900) elapses:

```bash
JOB=$(curl -s https://api.nolgia.ai/v1/generate/image \
  -H "Authorization: Bearer nol_..." \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-image-2","prompt":"a red fox in the snow"}' | jq -r .id)

curl -s "https://api.nolgia.ai/v1/jobs/$JOB/wait?timeout_seconds=120" \
  -H "Authorization: Bearer nol_..."
```

A `200` carries the terminal job; on success `job.asset` is the produced
asset, whose `signed_url` downloads the media (the URL stays valid for about
an hour, `expires_at` says exactly). A `408` means the timeout elapsed first:
the job is still running, call `wait` again. `GET /jobs/{id}` reads the job
without waiting, `GET /jobs` lists them, and `GET /assets?job_id={id}` returns
everything a job produced (a request with `num_images` greater than one
yields several assets).

Images typically settle in seconds, audio in tens of seconds, video in one to
several minutes depending on the model and duration. Every model's durations,
resolutions and reference slots are published on `GET /models`.

Two things keep retries safe. Submitting an identical request twice within
five minutes answers `409` naming the job that already exists, so a dropped
connection cannot turn into a second charge. To deliberately generate the
same prompt again, send a fresh `Idempotency-Key` header; repeat the same key
to make your own retries collapse onto one job.

## Credits: hold, then settle

Everything bills in credits, and the price list is public:
`GET /pricing/models` (no token) returns each model's cost and billing unit,
which is what [nolgia.ai/pricing](https://nolgia.ai/pricing) prints.

When a generation is accepted, its full price is **held** from your balance
at once; the hold is the charge, and a delivered generation writes no second
row. If the job fails for any reason other than a content policy rejection,
the hold is **released** and shows up as a refund for the same amount. A
content policy rejection consumes the hold, because the provider has already
billed for the attempt. `GET /billing/credits` reads the balance;
`GET /billing/transactions` is the itemized ledger, newest first, with the
running balance after each row.

A personal token spends the account's shared top-up pool. An empty pool is a
`402` titled `Payment Required`; top up at
[nolgia.ai/billing](https://nolgia.ai/billing). A model above your plan is
also a `402`, titled `Upgrade Required`, and says so in `detail`: those are
different problems and the response tells you which one you hit.

## Uploads

Reference images, start frames and reference videos are passed as URLs or as
ids of assets you already own. To get a file in:

- **Small images** (up to about 10 MB decoded): `POST /assets` with the bytes
  base64-encoded. The response is an asset whose `signed_url` can be handed
  to `image_url` on a generation request.
- **Anything larger** (video, audio, big stills): the signed-upload flow.
  `POST /assets/uploads` with `filename`, `content_type` and the exact
  `size_bytes` returns an `upload_url`; `PUT` the file to it with a
  `Content-Type` header that matches exactly (no Authorization header needed;
  the URL expires after 30 minutes); then `POST /assets/uploads/{upload_id}/complete`
  verifies the object and flips the asset to `ready`.

```bash
SLOT=$(curl -s https://api.nolgia.ai/v1/assets/uploads \
  -H "Authorization: Bearer nol_..." \
  -H "Content-Type: application/json" \
  -d '{"filename":"take-3.mp4","content_type":"video/mp4","size_bytes":48213977}')

curl -X PUT "$(echo "$SLOT" | jq -r .upload_url)" \
  -H "Content-Type: video/mp4" --data-binary @take-3.mp4

curl -X POST "https://api.nolgia.ai/v1/assets/uploads/$(echo "$SLOT" | jq -r .upload_id)/complete" \
  -H "Authorization: Bearer nol_..."
```

Uploading assets are hidden from `GET /assets` until completed. Limits: 50 GiB
for video, 5 GiB for audio, 100 MiB for images.

## Errors

Errors the API's handlers produce are RFC 7807 problem objects with the HTTP
status in the body:

```json
{
  "type": "about:blank",
  "title": "Invalid request",
  "status": 422,
  "detail": "`prompt` must be at least 1 character.",
  "request_id": "req_01J5R2M9YBX2K4Z3WQH6P8VNA1"
}
```

| Status | Meaning |
| --- | --- |
| `400` | A parameter the model cannot honor (a reference slot it lacks, a duration it does not render). `detail` names it. |
| `401` | Missing, revoked or malformed token. |
| `402` | `Payment Required` (empty credit pool) or `Upgrade Required` (plan floor). Read `title`. |
| `403` | Read-only role inside an organization, or a row you may not change. |
| `404` | Not yours or does not exist; the two are indistinguishable by design. |
| `408` | `GET /jobs/{id}/wait` timed out; the job is still running. |
| `409` | Duplicate submission within five minutes (`detail` names the existing job), or an upload whose object is missing or mis-sized. |
| `413` | Upload exceeds the size limit. |
| `422` | The body failed validation. |
| `429` | Rate limited, or over a plan quota. Honor `Retry-After` when present; a generation quota refusal sends `Retry-After-Reset` (an RFC 3339 time) instead; concurrency and agent queue limits send neither, so back off with jitter and read `detail`. |

Two caveats. `type` is `about:blank` and `instance` is absent today, so key
your handling on `status` and `title`, and quote `request_id` (when present)
if you write to support. And a request the router rejects before a handler
runs, such as a malformed path or query parameter or an unsupported method,
answers plain text, not JSON: check the status and `Content-Type` before
decoding a raw response.

The generated clients surface the problem object: Python returns an `Error`
model instead of the success type, TypeScript fills `error` in
`{ data, error }`, and Go fills the status-specific field for the operation
(`JSON401`, `JSON402`, ...) or `JSONDefault`. A non-JSON error leaves those
typed fields empty, so always handle the call's own error too.

## Client libraries

Each client README walks the same path as this page (install, authenticate,
first image, first video, list assets, staging):

- Python: [`sdk/python/README.md`](https://github.com/nolgiainc/nolgia-api/blob/main/sdk/python/README.md), package [`nolgia` on PyPI](https://pypi.org/project/nolgia/)
- TypeScript: [`sdk/typescript/README.md`](https://github.com/nolgiainc/nolgia-api/blob/main/sdk/typescript/README.md), package [`@nolgia/sdk` on npm](https://www.npmjs.com/package/@nolgia/sdk)
- Go: [`sdk/go/README.md`](https://github.com/nolgiainc/nolgia-api/blob/main/sdk/go/README.md), import path `github.com/nolgiainc/nolgia-api/sdk/go`
- Rust CLI: [`nolgia-cli` on crates.io](https://crates.io/crates/nolgia-cli), documented at [nolgia.ai/cli](https://nolgia.ai/cli)

The GitHub links above require access to the `nolgia-api` repository while it
is private; the PyPI and npm packages ship the same READMEs from version
`0.1.1`.

Next: the [API reference](../api/) for every endpoint and schema, and
[nolgia.ai/developers](https://nolgia.ai/developers) for the portal.
